From 608e9104c04cdaf28710a779278482dbd3bdd0be Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Tue, 5 Nov 2024 02:04:29 +0300 Subject: [PATCH 01/21] update docs and udp server --- go.mod | 2 +- go.sum | 4 +- pkg/admin_client/dev_doc/ru/udp/udp.go | 52 +- vendor/github.com/ascenmmo/udp-server/LICENSE | 695 +----------------- .../github.com/ascenmmo/udp-server/env/env.go | 10 +- .../internal/connection/notify_servers.go | 5 +- .../udp-server/internal/handler/udp/udp.go | 16 +- .../service/configs_service/count_results.go | 4 +- .../service/configs_service/game_configs.go | 19 +- .../udp-server/internal/service/service.go | 228 +++--- .../ascenmmo/udp-server/pkg/errors/errors.go | 2 + .../pkg/restconnection/types/network.go | 12 - vendor/modules.txt | 2 +- 13 files changed, 213 insertions(+), 838 deletions(-) diff --git a/go.mod b/go.mod index 8715697..a648efb 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.2 require ( github.com/ascenmmo/tcp-server v0.0.0-20241024213218-104f0955af07 github.com/ascenmmo/token-generator v1.0.0 - github.com/ascenmmo/udp-server v0.0.0-20241024213152-f807e90ce36c + github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117 github.com/ascenmmo/websocket-server v0.0.0-20241024213056-3c1271cc0529 github.com/go-kit/kit v0.13.0 github.com/gofiber/adaptor/v2 v2.2.1 diff --git a/go.sum b/go.sum index 580f874..3a67621 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/ascenmmo/tcp-server v0.0.0-20241024213218-104f0955af07 h1:sAVYrtUU+Q0 github.com/ascenmmo/tcp-server v0.0.0-20241024213218-104f0955af07/go.mod h1:14K6fwh/Sj958MMSPA+35xHzng0ZzVc2oq5kpXYLQbo= github.com/ascenmmo/token-generator v1.0.0 h1:+yUCIXfeO68tuWvYyStT4dnnYWfujfP4vPXqOrEf7No= github.com/ascenmmo/token-generator v1.0.0/go.mod h1:M3RdQKl1JfQeqM2hWNs0645G48GupB7idwdzQMkxlxM= -github.com/ascenmmo/udp-server v0.0.0-20241024213152-f807e90ce36c h1:6XLXv+VJ1pI5SbQhLlB2ynVcJ6y2UN1lQKh5tBb99V4= -github.com/ascenmmo/udp-server v0.0.0-20241024213152-f807e90ce36c/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= +github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117 h1:aCM/QFPRKMC1PKwZwtMmEqgJEXHojfOuXi7U/9eTXGA= +github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= github.com/ascenmmo/websocket-server v0.0.0-20241024213056-3c1271cc0529 h1:Rwgx3hEykeJMIy9R/abLIz652Y1ZYnQR1cgSUOCV714= github.com/ascenmmo/websocket-server v0.0.0-20241024213056-3c1271cc0529/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/pkg/admin_client/dev_doc/ru/udp/udp.go b/pkg/admin_client/dev_doc/ru/udp/udp.go index 42b49dd..d2e5149 100644 --- a/pkg/admin_client/dev_doc/ru/udp/udp.go +++ b/pkg/admin_client/dev_doc/ru/udp/udp.go @@ -10,21 +10,45 @@ func GetUPDConnectionsStruct() []types.DocStruct { "UDP — это очень быстрое решение, однако, есть недостатки: некоторые пакеты могут быть потеряны или доставлены не по порядку.", DockLists: []types.DocStructList{ { - Title: "Отправка и приём сообщений", - Description: "Для подключения к сервису необходимо знать IP-адрес и порт. " + + Title: "Этап подключения", + Description: "Для подключения к сервису необходимо знать IP-адрес и порт." + "Максимальное количество сообщений в секунду — 200. " + - "Пример команды подключения и отправки сообщения через UDP:", - RequestPath: "udp://127.0.0.1:4500", - Method: "udp", - RequestBody: `{ - "token": "ваш токен", - "data": "данные для отправки" -}`, - RequestBodyInfo: "Тело запроса содержит токен для аутентификации и данные, которые необходимо передать другим клиентам.", - ResponseBody: `{ - "data": "данные от других клиентов" -}`, - ResponseBodyInfo: "Ответ содержит данные, полученные от других клиентов, подключённых к той же комнате.", + "Так же нужно дождаться от сервреа ответа в виде id вашего клиента, UserID в ответе является подтверждением что подключение аутентифицированное." + + "Пример команды подключения", + RequestPath: "udp://ascenmmo.com:4500", + Method: "udp write", + RequestBody: `eyJhbGciOiJIUz11NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzA3NjA4ODYsIkluZm8iOnsiZ2FtZV9pZCI61jJhZDIyNDNhLThhN2UtMzkzMC1iNjEzLTg5YzY2NDk2YWFjZCIsInJvb21faWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJ1c2VyX2lkIjoiN2RkZmNiNzEtOWQ3OC0zMTczLWExNGQtNDVjZTc1ODIyNmM3IiwidHRsIjo5MDAwMDAwMDAwMDB9fQ.wmq1VH88zlws_tSAdvJjfXcdcoaxL8vT8W9gsRZrw_o$_$j`, + RequestBodyInfo: "Нужно отправить string без всякой обвертки несколько раз пока от сервера не вернется userID. Сервер должен понять что ваше подключение является валидным, иначе вы не являетесь подключенным пользователем.", + ResponseBody: `3fa85f64-5717-4562-b3fc-2c963f66afa6`, + ResponseBodyInfo: "Запустите Этап чтения. Прежде чем переходить к этапу отправки вам должен вернуться userID это является подтверждением что ваше соединение аутентифицированное", + }, + { + Title: "Этап чтения", + Description: "Используйте тот же канал подключения, не нужно создавать новый. " + + "Канал подключения на этапе чтения и отправки должен быть один." + + "Команда чтения ", + RequestPath: "udp://ascenmmo.com:4500", + Method: "udp read", + RequestBody: `Пренадлежит этапу отправки, при поралельном чтении не требует отправки данных'`, + RequestBodyInfo: "Тут мы должны слушать ответ", + ResponseBody: `data1 data2 data2 или dat data2 d$#3 dt9 или data`, + ResponseBodyInfo: "ответ может содержать как одно сообщение так и несколько, udp не гарантирует целосность сообщений и их порядок. " + + "Сервер работает по схеме рассылки при получении одного сообщения от ПервогоКлиента от дублирует Второму и Третьему сообщение. ", + }, + { + Title: "Этап отправки", + Description: "Используйте тот же канал подключения, не нужно создавать новый. " + + "Канал подключения на этапе чтения и отправки должен быть один." + + "Так как в первом этапе вы аутентифицировались на сервере вы можете отослать пакет" + + "Пример команды отправки", + RequestPath: "udp://ascenmmo.com:4500", + Method: "udp write", + RequestBody: `клиент 1 отправил сообщение 101, или он отправил json {"name": "MyUser1"}`, + RequestBodyInfo: "сервер работате только с даными []byte. Массив байт распределяется по всем клиентам", + ResponseBody: `клиент 2 отправил сообщение 15, или он отправил json {"name": "MyUser2"} клиент 3 отправил сообщение 53, или он отправил json {"name": "MyUser3"}`, + ResponseBodyInfo: "ответ может содержать как одно сообщение так и несколько, udp не гарантирует целосность сообщений и их порядок. " + + "Если сообщения идут нечитаемые, поиграйтесь с времеными рамками отправителя сообщений. " + + "Сервер не валидирует сообщения и не дробит их, а рассылает всем как есть.", }, }, } diff --git a/vendor/github.com/ascenmmo/udp-server/LICENSE b/vendor/github.com/ascenmmo/udp-server/LICENSE index f288702..b2f3011 100644 --- a/vendor/github.com/ascenmmo/udp-server/LICENSE +++ b/vendor/github.com/ascenmmo/udp-server/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2024 Temur Abdurakhmanov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ascenmmo/udp-server/env/env.go b/vendor/github.com/ascenmmo/udp-server/env/env.go index a9da977..c573851 100644 --- a/vendor/github.com/ascenmmo/udp-server/env/env.go +++ b/vendor/github.com/ascenmmo/udp-server/env/env.go @@ -1,9 +1,9 @@ package env var ( - ServerAddress = "127.0.0.1" - TCPPort = "8081" - UDPPort = "4500" - TokenKey = "_remember_token_mast_be_32_bytes" - MaxRequestPerSecond = 200 + ServerAddress = "127.0.0.1" // Server address + TCPPort = "8081" // Port for TCP connections + UDPPort = "4500" // Port for UDP connections + TokenKey = "_remember_token_must_be_32_bytes" // Unique token for authentication + MaxRequestPerSecond = 200 // Maximum number of requests per second ) diff --git a/vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go b/vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go index fe2b1d3..2edf21c 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go @@ -2,13 +2,12 @@ package connection import ( "encoding/json" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" "github.com/google/uuid" "net" ) type NotifyServers interface { - NotifyServers(ids []uuid.UUID, request types.Request) error + NotifyServers(ids []uuid.UUID, request []byte) error AddServer(ID uuid.UUID, addr string) error } @@ -27,7 +26,7 @@ type server struct { Add *net.UDPConn } -func (n *notifier) NotifyServers(ids []uuid.UUID, request types.Request) error { +func (n *notifier) NotifyServers(ids []uuid.UUID, request []byte) error { if len(n.servers) == 0 { return nil } diff --git a/vendor/github.com/ascenmmo/udp-server/internal/handler/udp/udp.go b/vendor/github.com/ascenmmo/udp-server/internal/handler/udp/udp.go index fff6a5f..1a7edae 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/handler/udp/udp.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/handler/udp/udp.go @@ -2,12 +2,10 @@ package udp import ( "context" - "encoding/json" "github.com/ascenmmo/udp-server/internal/connection" "github.com/ascenmmo/udp-server/internal/service" memoryDB "github.com/ascenmmo/udp-server/internal/storage" "github.com/ascenmmo/udp-server/internal/utils" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" "github.com/rs/zerolog" "net" "runtime" @@ -29,7 +27,7 @@ type WorkerUDP struct { type ChanUDPMessage struct { client connection.DataSender - request types.Request + request []byte } func (w *WorkerUDP) Listener(ctx context.Context) error { @@ -78,14 +76,6 @@ func (w *WorkerUDP) handleConnection(clientAddr *net.UDPAddr, buf []byte) error } }() - var request types.Request - - err := json.Unmarshal(buf, &request) - if err != nil { - w.logger.Error().Err(err).Msg("handleConnection Unmarshal") - return err - } - ds := connection.DataSender(&connection.UDPConnection{ ClientAddr: clientAddr, Conn: w.conn, @@ -94,7 +84,7 @@ func (w *WorkerUDP) handleConnection(clientAddr *net.UDPAddr, buf []byte) error select { case w.chMsg[w.counterChen()] <- ChanUDPMessage{ client: ds, - request: request, + request: buf, }: default: return nil //errors.New("counterChen is full") @@ -121,7 +111,7 @@ func (w *WorkerUDP) sendWorker(ctx context.Context, ch chan ChanUDPMessage) { err = user.Connection.Write(msg) if err != nil { w.logger.Warn().Err(err).Interface("senderWorker WriteToUDP", user.ID) - err := w.service.RemoveUser(chMsg.request.Token, user.ID) + err := w.service.RemoveUser(chMsg.client, user.ID) if err != nil { w.logger.Warn().Err(err).Interface("senderWorker RemoveUser", user.ID) } diff --git a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go b/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go index 027a59d..34496f5 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go @@ -42,7 +42,7 @@ func (c *IncrementResult) Execute(clientInfo tokentype.Info, config types.Sortin values = append(values, value) } - if len(values) != len(oldResult.Result) { + if len(values) != len(config.Params) { return oldResult, nil } @@ -82,7 +82,7 @@ func (c *DecrementResult) Execute(clientInfo tokentype.Info, config types.Sortin values = append(values, value) } - if len(values) != len(oldResult.Result) { + if len(values) != len(config.Params) { return oldResult, nil } diff --git a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go b/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go index 1a67f0b..360df8b 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go @@ -6,6 +6,7 @@ import ( memoryDB "github.com/ascenmmo/udp-server/internal/storage" "github.com/ascenmmo/udp-server/internal/utils" "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/google/uuid" ) type GameConfigsService interface { @@ -21,7 +22,7 @@ type gameConfig struct { } func (g *gameConfig) Do(token string, clientInfo tokentype.Info, gameConfig types.GameConfigs, data interface{}) { - if gameConfig.IsExists { + if !gameConfig.IsExists { return } results, _ := g.getOldResults(clientInfo) @@ -42,25 +43,25 @@ func (g *gameConfig) Do(token string, clientInfo tokentype.Info, gameConfig type func (g *gameConfig) GetDeletedRoomsResults(_ tokentype.Info, onlinePlayersTokens []string) (results []types.GameConfigResults, ok bool) { ids := g.storage.GetAllConnection() - uniqueTokens := make(map[string]struct{}, len(onlinePlayersTokens)) + uniqueTokens := make(map[string]struct{}) for _, token := range onlinePlayersTokens { uniqueTokens[token] = struct{}{} } - var notFoundTokens []string + var offlinePlayers []string for _, token := range ids { if _, exists := uniqueTokens[token]; !exists { - notFoundTokens = append(notFoundTokens, token) + offlinePlayers = append(offlinePlayers, token) } } clientsInfo := make(map[string]tokentype.Info) - for _, token := range onlinePlayersTokens { - info, err := g.token.ParseToken(token) - if err != nil { - continue + for _, token := range offlinePlayers { + info, _ := g.token.ParseToken(token) + if info.RoomID != uuid.Nil { + clientsInfo[info.RoomID.String()] = info } - clientsInfo[info.RoomID.String()] = info + } for _, info := range clientsInfo { diff --git a/vendor/github.com/ascenmmo/udp-server/internal/service/service.go b/vendor/github.com/ascenmmo/udp-server/internal/service/service.go index 94d31da..17c9af5 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/service/service.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/service/service.go @@ -1,7 +1,6 @@ package service import ( - "encoding/json" "fmt" tokengenerator "github.com/ascenmmo/token-generator/token_generator" tokentype "github.com/ascenmmo/token-generator/token_type" @@ -20,9 +19,9 @@ import ( type Service interface { GetConnectionsNum() (countConn int, exists bool) CreateRoom(token string, configs types.GameConfigs) error - GetUsersAndMessage(ds connection.DataSender, req types.Request) (users []entities.User, msg []byte, err error) - NotifyAllServers(clientInfo tokentype.Info, req types.Request) (err error) - RemoveUser(token string, userID uuid.UUID) (err error) + GetUsersAndMessage(ds connection.DataSender, req []byte) (users []entities.User, msg []byte, err error) + NotifyAllServers(clientInfo tokentype.Info, reqreq []byte) (err error) + RemoveUser(ds connection.DataSender, userID uuid.UUID) (err error) SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) GetGameResults(token string) (results []types.GameConfigResults, err error) } @@ -71,130 +70,98 @@ func (s *service) CreateRoom(token string, configs types.GameConfigs) error { } func (s *service) SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) { - clientInfo, err := s.token.ParseToken(token) - if err != nil { - return err - } - - room, err := s.getRoom(clientInfo) - if err != nil { - return err - } - - room.SetServerID(id) - - data, _ := s.storage.GetData(utils.GenerateNotifyServerKey()) - - server, ok := data.(connection.NotifyServers) - if !ok { - s.logger.Warn().Msg("NotifyServers cant get interfase") - server = connection.NewNotifierServers() - } - - err = server.AddServer(id, url) - if err != nil { - return err - } - - s.storage.SetData(utils.GenerateNotifyServerKey(), server) + //clientInfo, err := s.token.ParseToken(token) + //if err != nil { + // return err + //} + // + //room, err := s.getRoom(clientInfo) + //if err != nil { + // return err + //} + // + //room.SetServerID(id) + // + //data, _ := s.storage.GetData(utils.GenerateNotifyServerKey()) + // + //server, ok := data.(connection.NotifyServers) + //if !ok { + // s.logger.Warn().Msg("NotifyServers cant get interfase") + // server = connection.NewNotifierServers() + //} + // + //err = server.AddServer(id, url) + //if err != nil { + // return err + //} + // + //s.storage.SetData(utils.GenerateNotifyServerKey(), server) return nil - } -func (s *service) NotifyAllServers(clientInfo tokentype.Info, request types.Request) (err error) { - room, err := s.getRoom(clientInfo) - if err != nil { - return err - } - if len(room.ServerID) == 0 { - return nil - } - - data, ok := s.storage.GetData(utils.GenerateNotifyServerKey()) - if !ok { - return errors.ErrNotifyServerNotFound - } - - servers, ok := data.(connection.NotifyServers) - if !ok { - return errors.ErrNotifyServerNotValid - } - - err = servers.NotifyServers(room.ServerID, request) - if err != nil { - return err - } +func (s *service) NotifyAllServers(clientInfo tokentype.Info, request []byte) (err error) { + //room, err := s.getRoom(clientInfo) + //if err != nil { + // return err + //} + //if len(room.ServerID) == 0 { + // return nil + //} + // + //data, ok := s.storage.GetData(utils.GenerateNotifyServerKey()) + //if !ok { + // return errors.ErrNotifyServerNotFound + //} + // + //servers, ok := data.(connection.NotifyServers) + //if !ok { + // return errors.ErrNotifyServerNotValid + //} + // + //err = servers.NotifyServers(room.ServerID, request) + //if err != nil { + // return err + //} return nil } -func (s *service) GetUsersAndMessage(ds connection.DataSender, req types.Request) (users []entities.User, msg []byte, err error) { - clientInfo, err := s.token.ParseToken(req.Token) +func (s *service) GetUsersAndMessage(ds connection.DataSender, req []byte) (users []entities.User, msg []byte, err error) { + clientInfo, room, err := s.getRoom(ds) if err != nil { - return nil, nil, err + clientInfo, err = s.setNewUser(ds, req) + if err != nil { + return nil, nil, err + } + return append(users, entities.User{Connection: ds}), []byte(clientInfo.UserID.String()), nil } - room, err := s.getRoom(clientInfo) - if err != nil { - return nil, nil, err + if len(req) == 454 || len(req) == 343 { + return append(users, entities.User{Connection: ds}), []byte(clientInfo.UserID.String()), nil } - isNew := true usersData := room.GetUser() for _, v := range usersData { if v.ID == clientInfo.UserID && ds.GetID() == v.Connection.GetID() { - isNew = false continue } - users = append(users, *v) } - if isNew { - room.SetUser(&entities.User{ - ID: clientInfo.UserID, - Connection: ds, - }) - - s.storage.AddConnection(req.Token) - } - - response := types.Response{ - Data: req.Data, - } - - marshal, err := json.Marshal(response) - if err != nil { - return nil, nil, err - } - - if req.Server == nil { - s.gameConfigService.Do(req.Token, clientInfo, room.GameConfigs, req.Data) - id := uuid.New() - req.Server = &id - err = s.NotifyAllServers(clientInfo, req) - if err != nil { - s.logger.Warn().Err(err).Msg("NotifyAllServers err") - } - } + msg = req - return users, marshal, err + return users, msg, err } -func (s *service) RemoveUser(token string, userID uuid.UUID) (err error) { - clientInfo, err := s.token.ParseToken(token) +func (s *service) RemoveUser(ds connection.DataSender, userID uuid.UUID) (err error) { + _, room, err := s.getRoom(ds) if err != nil { return err } - game, err := s.getRoom(clientInfo) - if err != nil { - return err - } - - game.RemoveUser(userID) + room.RemoveUser(userID) return nil } @@ -214,17 +181,74 @@ func (s *service) GetGameResults(token string) (results []types.GameConfigResult return roomsResults, nil } -func (s *service) getRoom(clientInfo tokentype.Info) (room *entities.Room, err error) { +func (s *service) setNewUser(ds connection.DataSender, req []byte) (clientInfo *tokentype.Info, err error) { + token := string(req) + + info, err := s.token.ParseToken(token) + if err != nil { + return clientInfo, errors.ErrNewConnectionMastGetToken + } + clientInfo = &info + s.storage.SetData(ds.GetID(), info) + roomKey := utils.GenerateRoomKey(info) + + roomData, ok := s.storage.GetData(roomKey) + if !ok { + return clientInfo, errors.ErrRoomNotFound + } + + room, ok := roomData.(*entities.Room) + if !ok { + return clientInfo, errors.ErrRoomBadValue + } + + room.SetUser(&entities.User{ + ID: info.UserID, + Connection: ds, + }) + + s.storage.AddConnection(token) + + return clientInfo, nil +} + +func (s *service) getRoom(ds connection.DataSender) (clientInfo *tokentype.Info, room *entities.Room, err error) { + client, ok := s.storage.GetData(ds.GetID()) + if !ok { + return nil, nil, errors.ErrUserNotFound + } + + info, ok := client.(tokentype.Info) + if !ok { + return nil, nil, errors.ErrUserBadValue + } + + roomKey := utils.GenerateRoomKey(info) + + roomData, ok := s.storage.GetData(roomKey) + if !ok { + return nil, nil, errors.ErrRoomNotFound + } + + room, ok = roomData.(*entities.Room) + if !ok { + return nil, nil, errors.ErrRoomBadValue + } + + return &info, room, nil +} + +func (s *service) getRoomByClientInfo(clientInfo tokentype.Info) (room *entities.Room, err error) { roomKey := utils.GenerateRoomKey(clientInfo) roomData, ok := s.storage.GetData(roomKey) if !ok { - return room, errors.ErrRoomNotFound + return nil, errors.ErrRoomNotFound } room, ok = roomData.(*entities.Room) if !ok { - return room, errors.ErrRoomBadValue + return nil, errors.ErrRoomBadValue } return room, nil diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/errors/errors.go b/vendor/github.com/ascenmmo/udp-server/pkg/errors/errors.go index b5106c1..046a73a 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/errors/errors.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/errors/errors.go @@ -4,6 +4,8 @@ import "errors" var ( ErrUserNotFound = errors.New("user not found") + ErrNewConnectionMastGetToken = errors.New("new connection mast get new token") + ErrUserBadValue = errors.New("user bad value mast be reconnected") ErrRoomNotFound = errors.New("room not found") ErrRoomIsExists = errors.New("room is exists") ErrRoomBadValue = errors.New("room bad value") diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/network.go b/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/network.go index dd2dee8..58ad4bb 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/network.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/network.go @@ -1,17 +1,5 @@ package types -import "github.com/google/uuid" - -type Request struct { - Server *uuid.UUID `json:"server,omitempty"` - Token string `json:"token"` - Data any `json:"data"` -} - -type Response struct { - Data any `json:"data"` -} - type CreateRoomRequest struct { GameConfigs GameConfigs `json:"game_configs"` } diff --git a/vendor/modules.txt b/vendor/modules.txt index b69c4e4..98b8c24 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -24,7 +24,7 @@ github.com/ascenmmo/tcp-server/pkg/transport/viewer ## explicit; go 1.23.2 github.com/ascenmmo/token-generator/token_generator github.com/ascenmmo/token-generator/token_type -# github.com/ascenmmo/udp-server v0.0.0-20241024213152-f807e90ce36c +# github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117 ## explicit; go 1.23.2 github.com/ascenmmo/udp-server/env github.com/ascenmmo/udp-server/internal/connection From 2907e9158b9b418c8cd459f2533b0e36d383691b Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Fri, 8 Nov 2024 01:16:27 +0300 Subject: [PATCH 02/21] vendor --- go.mod | 6 +- go.sum | 12 +- internal/service/dev_tools/connections.go | 19 +- internal/service/scheduler/scheduler.go | 95 --- internal/start/multiplatform.go | 10 +- internal/start/rest.go | 2 +- internal/start/udp.go | 2 +- internal/start/ws.go | 2 +- pkg/multiplayer/types/server.go | 37 +- vendor/github.com/ascenmmo/tcp-server/LICENSE | 695 +----------------- .../github.com/ascenmmo/tcp-server/env/env.go | 8 - .../internal/entities/notify_servers.go | 77 -- .../tcp-server/internal/handler/connection.go | 2 +- .../internal/handler/serve_settings.go | 23 +- .../service/configs_service/count_results.go | 191 ----- .../service/configs_service/game_configs.go | 117 --- .../service/configs_service/numbers.go | 78 -- .../tcp-server/internal/service/service.go | 139 +--- .../tcp-server/internal/storage/memory_db.go | 26 +- .../{restconnection => api}/connections.go | 4 +- .../server_settings.go | 11 +- .../tcp-server/pkg/api/types/network.go | 13 + .../game.go => pkg/api/types/room.go} | 5 +- .../{restconnection => api}/types/settings.go | 0 .../pkg/clients/tcpGameServer/batch.go | 32 - .../pkg/clients/tcpGameServer/cache.go | 12 - .../pkg/clients/tcpGameServer/cb/breaker.go | 214 ------ .../pkg/clients/tcpGameServer/cb/count.go | 36 - .../pkg/clients/tcpGameServer/cb/default.go | 16 - .../pkg/clients/tcpGameServer/cb/error.go | 10 - .../pkg/clients/tcpGameServer/cb/option.go | 28 - .../pkg/clients/tcpGameServer/cb/setting.go | 14 - .../pkg/clients/tcpGameServer/cb/state.go | 27 - .../pkg/clients/tcpGameServer/cb/tscb.go | 35 - .../pkg/clients/tcpGameServer/error.go | 25 - .../tcpGameServer/gameconnections-exchange.go | 31 - .../tcpGameServer/gameconnections-fallback.go | 8 - .../tcpGameServer/gameconnections-jsonrpc.go | 190 ----- .../clients/tcpGameServer/hasher/errors.go | 13 - .../clients/tcpGameServer/hasher/hasher.go | 28 - .../clients/tcpGameServer/hasher/include.go | 13 - .../clients/tcpGameServer/hasher/option.go | 42 -- .../clients/tcpGameServer/hasher/walker.go | 261 ------- .../pkg/clients/tcpGameServer/jsonrpc.go | 77 -- .../clients/tcpGameServer/jsonrpc/client.go | 38 - .../clients/tcpGameServer/jsonrpc/error.go | 30 - .../tcpGameServer/jsonrpc/http2curl.go | 58 -- .../clients/tcpGameServer/jsonrpc/internal.go | 182 ----- .../clients/tcpGameServer/jsonrpc/option.go | 51 -- .../clients/tcpGameServer/jsonrpc/param.go | 42 -- .../clients/tcpGameServer/jsonrpc/public.go | 53 -- .../clients/tcpGameServer/jsonrpc/request.go | 36 - .../clients/tcpGameServer/jsonrpc/response.go | 58 -- .../clients/tcpGameServer/jsonrpc/string.go | 16 - .../pkg/clients/tcpGameServer/options.go | 76 -- .../tcpGameServer/serversettings-exchange.go | 57 -- .../tcpGameServer/serversettings-fallback.go | 11 - .../tcpGameServer/serversettings-jsonrpc.go | 354 --------- .../pkg/clients/tcpGameServer/tracer.go | 36 - .../pkg/clients/tcpGameServer/version.go | 4 - .../ascenmmo/tcp-server/pkg/errors/errors.go | 2 +- .../pkg/restconnection/types/game_config.go | 45 -- .../pkg/restconnection/types/network.go | 18 - .../ascenmmo/tcp-server/pkg/start/start.go | 28 +- .../pkg/transport/gameconnections-exchange.go | 2 +- .../pkg/transport/gameconnections-http.go | 6 +- .../pkg/transport/gameconnections-logger.go | 8 +- .../transport/gameconnections-middleware.go | 6 +- .../pkg/transport/gameconnections-server.go | 8 +- .../pkg/transport/gameconnections-trace.go | 8 +- .../tcp-server/pkg/transport/jsonrpc.go | 4 - .../pkg/transport/serversettings-exchange.go | 22 +- .../pkg/transport/serversettings-http.go | 8 +- .../pkg/transport/serversettings-jsonrpc.go | 118 --- .../pkg/transport/serversettings-logger.go | 51 +- .../transport/serversettings-middleware.go | 11 +- .../pkg/transport/serversettings-server.go | 33 +- .../pkg/transport/serversettings-trace.go | 21 +- .../github.com/ascenmmo/udp-server/env/env.go | 9 - .../internal/connection/notify_servers.go | 98 --- .../internal/handler/tcp/server_settings.go | 18 +- .../service/configs_service/count_results.go | 191 ----- .../service/configs_service/game_configs.go | 118 --- .../service/configs_service/numbers.go | 78 -- .../udp-server/internal/service/service.go | 129 +--- .../udp-server/internal/storage/memory_db.go | 26 +- .../server_settings.go | 10 +- .../{restconnection => api}/types/network.go | 1 - .../entities => pkg/api/types}/room.go | 5 +- .../{restconnection => api}/types/settings.go | 4 - .../udpGameServer/serversettings-exchange.go | 10 +- .../udpGameServer/serversettings-fallback.go | 1 - .../udpGameServer/serversettings-jsonrpc.go | 56 +- .../pkg/restconnection/types/game_config.go | 45 -- .../ascenmmo/udp-server/pkg/start/start.go | 28 +- .../udp-server/pkg/transport/jsonrpc.go | 2 - .../pkg/transport/serversettings-exchange.go | 10 +- .../pkg/transport/serversettings-http.go | 7 +- .../pkg/transport/serversettings-jsonrpc.go | 59 -- .../pkg/transport/serversettings-logger.go | 27 +- .../transport/serversettings-middleware.go | 8 +- .../pkg/transport/serversettings-server.go | 20 +- .../pkg/transport/serversettings-trace.go | 14 +- .../ascenmmo/websocket-server/LICENSE | 20 +- .../ascenmmo/websocket-server/env/env.go | 9 - .../internal/connection/dataSender.go | 1 + .../internal/connection/notify_servers.go | 97 --- .../internal/connection/ws.go | 14 +- .../internal/handler/tcp/server_settings.go | 23 +- .../internal/handler/ws/ws.go | 85 +-- .../service/configs_service/count_results.go | 191 ----- .../service/configs_service/game_configs.go | 117 --- .../service/configs_service/numbers.go | 78 -- .../internal/service/service.go | 163 +--- .../internal/storage/memory_db.go | 26 +- .../server_settings.go | 11 +- .../websocket-server/pkg/api/types/network.go | 4 + .../pkg/{entities => api/types}/room.go | 5 +- .../{restconnection => api}/types/settings.go | 5 - .../pkg/restconnection/types/game_config.go | 45 -- .../pkg/restconnection/types/network.go | 19 - .../websocket-server/pkg/start/start.go | 31 +- .../websocket-server/pkg/transport/jsonrpc.go | 4 - .../pkg/transport/serversettings-exchange.go | 22 +- .../pkg/transport/serversettings-http.go | 8 +- .../pkg/transport/serversettings-jsonrpc.go | 118 --- .../pkg/transport/serversettings-logger.go | 51 +- .../transport/serversettings-middleware.go | 11 +- .../pkg/transport/serversettings-server.go | 33 +- .../pkg/transport/serversettings-trace.go | 21 +- vendor/modules.txt | 28 +- 131 files changed, 392 insertions(+), 6009 deletions(-) delete mode 100644 internal/service/scheduler/scheduler.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/env/env.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/internal/entities/notify_servers.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/count_results.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/game_configs.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/numbers.go rename vendor/github.com/ascenmmo/tcp-server/pkg/{restconnection => api}/connections.go (92%) rename vendor/github.com/ascenmmo/tcp-server/pkg/{restconnection => api}/server_settings.go (71%) create mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/api/types/network.go rename vendor/github.com/ascenmmo/tcp-server/{internal/entities/game.go => pkg/api/types/room.go} (93%) rename vendor/github.com/ascenmmo/tcp-server/pkg/{restconnection => api}/types/settings.go (100%) delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/batch.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cache.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/breaker.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/count.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/default.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/error.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/option.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/setting.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/state.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/tscb.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/error.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-exchange.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-fallback.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-jsonrpc.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/errors.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/hasher.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/include.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/option.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/walker.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/client.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/error.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/http2curl.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/internal.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/option.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/param.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/public.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/request.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/response.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/string.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/options.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-exchange.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-fallback.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-jsonrpc.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/tracer.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/version.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/game_config.go delete mode 100644 vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/network.go delete mode 100644 vendor/github.com/ascenmmo/udp-server/env/env.go delete mode 100644 vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go delete mode 100644 vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go delete mode 100644 vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go delete mode 100644 vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/numbers.go rename vendor/github.com/ascenmmo/udp-server/pkg/{restconnection => api}/server_settings.go (79%) rename vendor/github.com/ascenmmo/udp-server/pkg/{restconnection => api}/types/network.go (51%) rename vendor/github.com/ascenmmo/udp-server/{internal/entities => pkg/api/types}/room.go (93%) rename vendor/github.com/ascenmmo/udp-server/pkg/{restconnection => api}/types/settings.go (82%) delete mode 100644 vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/game_config.go delete mode 100644 vendor/github.com/ascenmmo/websocket-server/env/env.go delete mode 100644 vendor/github.com/ascenmmo/websocket-server/internal/connection/notify_servers.go delete mode 100644 vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/count_results.go delete mode 100644 vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/game_configs.go delete mode 100644 vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/numbers.go rename vendor/github.com/ascenmmo/websocket-server/pkg/{restconnection => api}/server_settings.go (70%) create mode 100644 vendor/github.com/ascenmmo/websocket-server/pkg/api/types/network.go rename vendor/github.com/ascenmmo/websocket-server/pkg/{entities => api/types}/room.go (93%) rename vendor/github.com/ascenmmo/websocket-server/pkg/{restconnection => api}/types/settings.go (82%) delete mode 100644 vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/game_config.go delete mode 100644 vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/network.go diff --git a/go.mod b/go.mod index a648efb..da00956 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/ascenmmo/multiplayer-game-servers go 1.23.2 require ( - github.com/ascenmmo/tcp-server v0.0.0-20241024213218-104f0955af07 + github.com/ascenmmo/tcp-server v1.0.1 github.com/ascenmmo/token-generator v1.0.0 - github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117 - github.com/ascenmmo/websocket-server v0.0.0-20241024213056-3c1271cc0529 + github.com/ascenmmo/udp-server v1.0.1 + github.com/ascenmmo/websocket-server v1.0.1 github.com/go-kit/kit v0.13.0 github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.52.5 diff --git a/go.sum b/go.sum index 3a67621..d7ac0d7 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,14 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/ascenmmo/tcp-server v0.0.0-20241024213218-104f0955af07 h1:sAVYrtUU+Q0OECrtxmp+AouR9n7tAchjBmH7gG6md2E= -github.com/ascenmmo/tcp-server v0.0.0-20241024213218-104f0955af07/go.mod h1:14K6fwh/Sj958MMSPA+35xHzng0ZzVc2oq5kpXYLQbo= +github.com/ascenmmo/tcp-server v1.0.1 h1:0G+GeJRzZRAX4Nkj0DeWH7FNV4z0JwdlI52rZiIcD6g= +github.com/ascenmmo/tcp-server v1.0.1/go.mod h1:14K6fwh/Sj958MMSPA+35xHzng0ZzVc2oq5kpXYLQbo= github.com/ascenmmo/token-generator v1.0.0 h1:+yUCIXfeO68tuWvYyStT4dnnYWfujfP4vPXqOrEf7No= github.com/ascenmmo/token-generator v1.0.0/go.mod h1:M3RdQKl1JfQeqM2hWNs0645G48GupB7idwdzQMkxlxM= -github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117 h1:aCM/QFPRKMC1PKwZwtMmEqgJEXHojfOuXi7U/9eTXGA= -github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= -github.com/ascenmmo/websocket-server v0.0.0-20241024213056-3c1271cc0529 h1:Rwgx3hEykeJMIy9R/abLIz652Y1ZYnQR1cgSUOCV714= -github.com/ascenmmo/websocket-server v0.0.0-20241024213056-3c1271cc0529/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= +github.com/ascenmmo/udp-server v1.0.1 h1:bOKY/+IJt9wesYYUh5GEmWwgSXpffN+eSKBqdL+/Hc4= +github.com/ascenmmo/udp-server v1.0.1/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= +github.com/ascenmmo/websocket-server v1.0.1 h1:MKwqJ6gtS++cjJ9RNaMWweGmDKw1ux1LiCUjjBQ2PVQ= +github.com/ascenmmo/websocket-server v1.0.1/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/internal/service/dev_tools/connections.go b/internal/service/dev_tools/connections.go index eba3a72..a91f4f1 100644 --- a/internal/service/dev_tools/connections.go +++ b/internal/service/dev_tools/connections.go @@ -14,12 +14,11 @@ import ( ) type connections struct { - gameStorage storage.GameStorage - serverStorage storage.ServersStorage - roomsStorage storage.RoomsStorage - gameConfigsStorage storage.GameConfigsStorage - token tokengenerator.TokenGenerator - logger *zerolog.Logger + gameStorage storage.GameStorage + serverStorage storage.ServersStorage + roomsStorage storage.RoomsStorage + token tokengenerator.TokenGenerator + logger *zerolog.Logger } func (c *connections) CreateRoom(ctx context.Context, token string, gameID uuid.UUID) (newToken string, err error) { @@ -144,14 +143,12 @@ func (c *connections) GetRoomsConnectionUrls(ctx context.Context, token string) return []types.ConnectionServer{}, errors.ErrServerCreatingRoomAllServesOffError } - config, err := c.gameConfigsStorage.GetConfig(info.GameID) if err != nil { c.logger.Error().Err(err).Msg("game configs not found") } for _, server := range servers { - configForServer := config.ConfigForServer(server.ServerType) - err = server.CreateRoom(ctx, token, configForServer) + err = server.CreateRoom(ctx, token) if err != nil { if err.Error() != errors.ErrRoomIsExists.Error() { c.logger.Error().Err(err).Msg("server error create room") @@ -187,6 +184,6 @@ func (c *connections) RemoveRoomByID(ctx context.Context, token string, gameID u return nil } -func NewConnections(gameStorage storage.GameStorage, serverStorage storage.ServersStorage, roomsStorage storage.RoomsStorage, gameConfigsStorage storage.GameConfigsStorage, token tokengenerator.TokenGenerator, logger *zerolog.Logger) multiplayer.DevToolsConnections { - return &connections{gameStorage: gameStorage, serverStorage: serverStorage, roomsStorage: roomsStorage, gameConfigsStorage: gameConfigsStorage, token: token, logger: logger} +func NewConnections(gameStorage storage.GameStorage, serverStorage storage.ServersStorage, roomsStorage storage.RoomsStorage, token tokengenerator.TokenGenerator, logger *zerolog.Logger) multiplayer.DevToolsConnections { + return &connections{gameStorage: gameStorage, serverStorage: serverStorage, roomsStorage: roomsStorage, token: token, logger: logger} } diff --git a/internal/service/scheduler/scheduler.go b/internal/service/scheduler/scheduler.go deleted file mode 100644 index 8c4b6c5..0000000 --- a/internal/service/scheduler/scheduler.go +++ /dev/null @@ -1,95 +0,0 @@ -package scheduler - -import ( - "context" - "github.com/ascenmmo/multiplayer-game-servers/internal/storage" - tokengenerator "github.com/ascenmmo/token-generator/token_generator" - tokentype "github.com/ascenmmo/token-generator/token_type" - "github.com/google/uuid" - "github.com/rs/zerolog/log" - "time" -) - -type Scheduler interface { - Run(ctx context.Context) -} - -type scheduler struct { - gameStorage storage.GameStorage - roomStorage storage.RoomsStorage - serverStorage storage.ServersStorage - confResultStorage storage.GameConfigsResultsStorage - tokenGenerator tokengenerator.TokenGenerator -} - -func (s scheduler) Run(ctx context.Context) { - ticker := time.NewTicker(time.Second * 1) - for range ticker.C { - err := s.getConfigResultsFromServer(ctx) - if err != nil { - log.Error().Err(err).Msg("error getting config results from server") - } - } -} - -func (s scheduler) getConfigResultsFromServer(ctx context.Context) (err error) { - serverIDs, err := s.serverStorage.FindAllServerIDs() - if err != nil { - return - } - - token, _ := s.tokenGenerator.GenerateToken(tokentype.Info{ - GameID: uuid.New(), - RoomID: uuid.New(), - UserID: uuid.New(), - }, tokengenerator.JWT) - - var batchServerIDs [][]uuid.UUID - batchSize := 10 - - for i := 0; i < len(serverIDs); i += batchSize { - end := i + batchSize - - if end > len(serverIDs) { - end = len(serverIDs) - } - - batchServerIDs = append(batchServerIDs, serverIDs[i:end]) - } - - for _, batch := range batchServerIDs { - servers, err := s.serverStorage.FindByIDs(batch) - if err != nil { - continue - } - - for _, server := range servers { - results, err := server.GetGameConfigResults(ctx, token) - if err != nil { - continue - } - - if len(results) == 0 { - continue - } - - err = s.confResultStorage.CreateMany(results) - if err != nil { - return err - } - - for _, result := range results { - err := s.roomStorage.Delete(result.RoomID) - if err != nil { - return err - } - } - } - } - - return nil -} - -func NewScheduler(gameStorage storage.GameStorage, roomStorage storage.RoomsStorage, serverStorage storage.ServersStorage, confResultStorage storage.GameConfigsResultsStorage, tokenGenerator tokengenerator.TokenGenerator) *scheduler { - return &scheduler{gameStorage: gameStorage, roomStorage: roomStorage, serverStorage: serverStorage, confResultStorage: confResultStorage, tokenGenerator: tokenGenerator} -} diff --git a/internal/start/multiplatform.go b/internal/start/multiplatform.go index 166d1cb..ca5a5fe 100644 --- a/internal/start/multiplatform.go +++ b/internal/start/multiplatform.go @@ -1,13 +1,11 @@ package start import ( - "context" "fmt" "github.com/ascenmmo/multiplayer-game-servers/env" "github.com/ascenmmo/multiplayer-game-servers/internal/service/access" devtools "github.com/ascenmmo/multiplayer-game-servers/internal/service/dev_tools" "github.com/ascenmmo/multiplayer-game-servers/internal/service/registration" - "github.com/ascenmmo/multiplayer-game-servers/internal/service/scheduler" "github.com/ascenmmo/multiplayer-game-servers/internal/storage" adminclient "github.com/ascenmmo/multiplayer-game-servers/pkg/admin_client" "github.com/ascenmmo/multiplayer-game-servers/pkg/admin_client/dev_doc" @@ -40,17 +38,13 @@ func Multiplayer(logger zerolog.Logger) { mastNil(err) gameConfigStorage, err := storage.NewGameConfigsStorage(client) mastNil(err) - gameConfigResultsStorage, err := storage.NewGameConfigsResultsStorage(client) - mastNil(err) accessGameService := access.NewAccessGame(accessGameStorage) - newScheduler := scheduler.NewScheduler(gameStorage, roomStorage, serverStorage, gameConfigResultsStorage, token) - developerService := registration.NewDeveloperService(developerStorage, token, &logger) clientService := registration.NewClientService(clientStorage, gameStorage, roomStorage, token, &logger) - devToolsConnectionServicc := devtools.NewConnections(gameStorage, serverStorage, roomStorage, gameConfigStorage, token, &logger) + devToolsConnectionServicc := devtools.NewConnections(gameStorage, serverStorage, roomStorage, token, &logger) devToolsService := devtools.NewDevTools(accessGameService, gameStorage, serverStorage, token, &logger) devToolsServerService := devtools.NewServerService(accessGameService, gameStorage, serverStorage, token, &logger) devToolsGameConfigs := devtools.NewGameConfigs(accessGameService, gameConfigStorage, token, &logger) @@ -72,8 +66,6 @@ func Multiplayer(logger zerolog.Logger) { adminPanel(app) } - go newScheduler.Run(context.Background()) - logger.Info().Str("bind", fmt.Sprintf("http://%s:%s", env.ServerAddress, env.MultiplayerPort)).Msg("listen on") if err := srv.Fiber().Listen(":" + env.MultiplayerPort); err != nil { logger.Panic().Err(err).Stack().Msg("server error") diff --git a/internal/start/rest.go b/internal/start/rest.go index 9709aa4..9874d5a 100644 --- a/internal/start/rest.go +++ b/internal/start/rest.go @@ -15,8 +15,8 @@ func TcpServer(ctx context.Context, logger zerolog.Logger) { env.TokenKey, env.TcpServerMaxRequestPerSecond, 10, - 60, logger.With().Str("server:", "tcp").Logger(), + false, ) mastNil(err) } diff --git a/internal/start/udp.go b/internal/start/udp.go index 42e6946..458b135 100644 --- a/internal/start/udp.go +++ b/internal/start/udp.go @@ -16,8 +16,8 @@ func UdpServer(ctx context.Context, logger zerolog.Logger) { env.TokenKey, env.UdpServerMaxRequestPerSecond, 10, - 60, logger.With().Str("server:", "udp").Logger(), + false, ) mastNil(err) } diff --git a/internal/start/ws.go b/internal/start/ws.go index c2fd444..fa6def3 100644 --- a/internal/start/ws.go +++ b/internal/start/ws.go @@ -16,8 +16,8 @@ func WebsocketRun(ctx context.Context, logger zerolog.Logger) { env.TokenKey, env.WebsocketServerMaxRequestPerSecond, 10, - 60, logger.With().Str("server:", "websocket").Logger(), + false, ) mastNil(err) } diff --git a/pkg/multiplayer/types/server.go b/pkg/multiplayer/types/server.go index 1d498f9..638d28e 100644 --- a/pkg/multiplayer/types/server.go +++ b/pkg/multiplayer/types/server.go @@ -2,10 +2,8 @@ package types import ( "context" - "encoding/json" "fmt" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer" - restType "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/ascenmmo/udp-server/pkg/clients/udpGameServer" "github.com/google/uuid" "strings" @@ -62,20 +60,10 @@ func (s *Server) IsExists(ctx context.Context, token string) (bool, error) { return exists, err } -func (s *Server) CreateRoom(ctx context.Context, token string, gameConfigs GameConfigs) (err error) { - cli := tcpGameServer.New(s.getRestUrl()) - - confBuf, err := json.Marshal(gameConfigs) - - var config restType.GameConfigs - err = json.Unmarshal(confBuf, &config) - if err != nil { - return err - } +func (s *Server) CreateRoom(ctx context.Context, token string) (err error) { + cli := udpGameServer.New(s.getRestUrl()) - err = cli.ServerSettings().CreateRoom(ctx, token, restType.CreateRoomRequest{ - GameConfigs: config, - }) + err = cli.ServerSettings().CreateRoom(ctx, token, types.CreateRoomRequest{}) if err != nil { return err } @@ -83,23 +71,6 @@ func (s *Server) CreateRoom(ctx context.Context, token string, gameConfigs GameC return nil } -func (s *Server) GetGameConfigResults(ctx context.Context, token string) (gameResults []GameConfigResults, err error) { - cli := tcpGameServer.New(s.getRestUrl()) - results, err := cli.ServerSettings().GetGameResults(ctx, token) - if err != nil { - return nil, err - } - for _, v := range results { - gameResults = append(gameResults, GameConfigResults{ - ID: uuid.New(), - GameID: v.GameID, - RoomID: v.RoomID, - Result: v.Result, - }) - } - return gameResults, err -} - func (s *Server) RemoveOwner(ownerID uuid.UUID) { for i, ownersID := range s.Owners { if ownersID == ownerID { diff --git a/vendor/github.com/ascenmmo/tcp-server/LICENSE b/vendor/github.com/ascenmmo/tcp-server/LICENSE index f288702..b2f3011 100644 --- a/vendor/github.com/ascenmmo/tcp-server/LICENSE +++ b/vendor/github.com/ascenmmo/tcp-server/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2024 Temur Abdurakhmanov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ascenmmo/tcp-server/env/env.go b/vendor/github.com/ascenmmo/tcp-server/env/env.go deleted file mode 100644 index 4c59c5c..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/env/env.go +++ /dev/null @@ -1,8 +0,0 @@ -package env - -var ( - ServerAddress = "0.0.0.0" - TCPPort = "8083" - TokenKey = "_remember_token_mast_be_32_bytes" - MaxRequestPerSecond = 5 -) diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/entities/notify_servers.go b/vendor/github.com/ascenmmo/tcp-server/internal/entities/notify_servers.go deleted file mode 100644 index d8566df..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/internal/entities/notify_servers.go +++ /dev/null @@ -1,77 +0,0 @@ -package entities - -import ( - "context" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" -) - -type NotifyServers interface { - NotifyServers(ids []uuid.UUID, req types.RequestSetMessage) error - AddServer(ID uuid.UUID, token, addr string) error -} - -type notifier struct { - servers []*server -} - -func NewNotifierServers() NotifyServers { - return ¬ifier{} -} - -type server struct { - ID uuid.UUID `json:"id"` - Addr string `json:"addr"` -} - -func (n *notifier) NotifyServers(ids []uuid.UUID, req types.RequestSetMessage) error { - for _, id := range ids { - for _, server := range n.servers { - if server.ID == id { - err := tcpGameServer.New(server.Addr).GameConnections().SetSendMessage(context.Background(), req.Token, req) - if err != nil { - n.RemoveNotifyServer(server.ID) - return err - } - } - } - } - return nil -} - -func (n *notifier) AddServer(ID uuid.UUID, token string, addr string) error { - newServer := &server{ - ID: ID, - Addr: addr, - } - err := newServer.Connect(token) - if err != nil { - return err - } - for i, s := range n.servers { - if s.ID == ID { - n.servers[i] = newServer - return nil - } - } - n.servers = append(n.servers, newServer) - return nil -} - -func (n *notifier) RemoveNotifyServer(id uuid.UUID) { - for i, s := range n.servers { - if s.ID == id { - n.servers = append(n.servers[:i], n.servers[i+1:]...) - } - } -} - -func (s *server) Connect(token string) error { - cli := tcpGameServer.New(s.Addr) - _, err := cli.ServerSettings().HealthCheck(context.Background(), token) - if err != nil { - return err - } - return nil -} diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/handler/connection.go b/vendor/github.com/ascenmmo/tcp-server/internal/handler/connection.go index 0e0a624..1667abc 100644 --- a/vendor/github.com/ascenmmo/tcp-server/internal/handler/connection.go +++ b/vendor/github.com/ascenmmo/tcp-server/internal/handler/connection.go @@ -4,8 +4,8 @@ import ( "context" "github.com/ascenmmo/tcp-server/internal/service" "github.com/ascenmmo/tcp-server/internal/utils" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/ascenmmo/tcp-server/pkg/errors" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" "github.com/google/uuid" ) diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/handler/serve_settings.go b/vendor/github.com/ascenmmo/tcp-server/internal/handler/serve_settings.go index 1b8d8ba..fb7a26e 100644 --- a/vendor/github.com/ascenmmo/tcp-server/internal/handler/serve_settings.go +++ b/vendor/github.com/ascenmmo/tcp-server/internal/handler/serve_settings.go @@ -4,9 +4,8 @@ import ( "context" "github.com/ascenmmo/tcp-server/internal/service" "github.com/ascenmmo/tcp-server/internal/utils" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/ascenmmo/tcp-server/pkg/errors" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" ) type ServerSettings struct { @@ -45,25 +44,7 @@ func (r *ServerSettings) CreateRoom(ctx context.Context, token string, createRoo if limited { return errors.ErrTooManyRequests } - err = r.server.CreateRoom(token, createRoom.GameConfigs) - return -} - -func (r *ServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - limited := r.rateLimit.IsLimited(token) - if limited { - return gameConfigResults, errors.ErrTooManyRequests - } - gameConfigResults, err = r.server.GetGameResults(token) - return -} - -func (r *ServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - limited := r.rateLimit.IsLimited(token) - if limited { - return errors.ErrTooManyRequests - } - err = r.server.SetRoomNotifyServer(token, id, url) + err = r.server.CreateRoom(token) return } diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/count_results.go b/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/count_results.go deleted file mode 100644 index 5cbcf4c..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/count_results.go +++ /dev/null @@ -1,191 +0,0 @@ -package configsService - -import ( - "github.com/ascenmmo/tcp-server/pkg/errors" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - tokentype "github.com/ascenmmo/token-generator/token_type" -) - -func GetCountingFunctions() (functions []types.GameConfigExecutor) { - functions = []types.GameConfigExecutor{ - &IncrementResult{}, - &DecrementResult{}, - &AdditionDataResultToOld{}, - &SubtractDataResultToOld{}, - } - return functions -} - -type IncrementResult struct { -} - -func (c *IncrementResult) Name() string { - return "IncrementResult" -} - -func (c *IncrementResult) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(oldResult.Result) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - oldResult.Result[config.ResultName] = incrementValue(oldValue) - - return oldResult, nil -} - -type DecrementResult struct { -} - -func (c *DecrementResult) Name() string { - return "DecrementResult" -} - -func (c *DecrementResult) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(oldResult.Result) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - oldResult.Result[config.ResultName] = decrementValue(oldValue) - - return oldResult, nil -} - -type AdditionDataResultToOld struct{} - -func (c *AdditionDataResultToOld) Name() string { - return "AdditionDataResultToOld" -} - -func (c *AdditionDataResultToOld) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - for _, newValue := range values { - oldValue = additionValues(oldValue, newValue, config.ResultType) - } - - oldResult.Result[config.ResultName] = oldValue - - return oldResult, nil -} - -type SubtractDataResultToOld struct{} - -func (c *SubtractDataResultToOld) Name() string { - return "SubtractDataResultToOld" -} - -func (c *SubtractDataResultToOld) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - for _, newValue := range values { - oldValue = subtractValues(oldValue, newValue, config.ResultType) - } - - oldResult.Result[config.ResultName] = oldValue - - return oldResult, nil -} - -func parseUserData(newUserData interface{}) (map[string]interface{}, error) { - data, ok := newUserData.(map[string]interface{}) - if !ok { - return nil, errors.ErrGameConfigMarshalUserData - } - return data, nil -} diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/game_configs.go b/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/game_configs.go deleted file mode 100644 index cd99b43..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/game_configs.go +++ /dev/null @@ -1,117 +0,0 @@ -package configsService - -import ( - memoryDB "github.com/ascenmmo/tcp-server/internal/storage" - "github.com/ascenmmo/tcp-server/internal/utils" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - tokengenerator "github.com/ascenmmo/token-generator/token_generator" - tokentype "github.com/ascenmmo/token-generator/token_type" -) - -type GameConfigsService interface { - Do(token string, clientInfo tokentype.Info, gameConfig types.GameConfigs, data interface{}) - GetDeletedRoomsResults(clientInfo tokentype.Info, onlinePlayersTokens []string) (results []types.GameConfigResults, ok bool) - SetServerExecuteToGameConfig(clientInfo tokentype.Info, gameConfig types.GameConfigs) (newGameConfig types.GameConfigs) -} - -type gameConfig struct { - allGameConfigExecutor []types.GameConfigExecutor - token tokengenerator.TokenGenerator - storage memoryDB.IMemoryDB -} - -func (g *gameConfig) Do(token string, clientInfo tokentype.Info, gameConfig types.GameConfigs, data interface{}) { - if gameConfig.IsExists { - return - } - results, _ := g.getOldResults(clientInfo) - for _, sorting := range gameConfig.SortingConfig { - if sorting.Executor == nil { - continue - } - newResult, err := sorting.Executor.Execute(clientInfo, sorting, data, results) - if err != nil { - continue - } - results = newResult - } - g.storage.AddConnection(token) - g.serOldResults(clientInfo, results) -} - -func (g *gameConfig) GetDeletedRoomsResults(_ tokentype.Info, onlinePlayersTokens []string) (results []types.GameConfigResults, ok bool) { - ids := g.storage.GetAllConnection() - - uniqueTokens := make(map[string]struct{}, len(onlinePlayersTokens)) - for _, token := range onlinePlayersTokens { - uniqueTokens[token] = struct{}{} - } - - var notFoundTokens []string - for _, token := range ids { - if _, exists := uniqueTokens[token]; !exists { - notFoundTokens = append(notFoundTokens, token) - } - } - - clientsInfo := make(map[string]tokentype.Info) - for _, token := range onlinePlayersTokens { - info, err := g.token.ParseToken(token) - if err != nil { - continue - } - clientsInfo[info.RoomID.String()] = info - } - - for _, info := range clientsInfo { - configResults, ok := g.getOldResults(info) - if !ok { - continue - } - results = append(results, configResults) - } - - return results, len(results) > 0 -} - -func (g *gameConfig) SetServerExecuteToGameConfig(_ tokentype.Info, gameConfig types.GameConfigs) (newGameConfig types.GameConfigs) { - isConfigExecutorFound := false - for i, conf := range gameConfig.SortingConfig { - for _, executor := range g.allGameConfigExecutor { - if executor.Name() == conf.Name { - gameConfig.SortingConfig[i].Executor = executor - isConfigExecutorFound = true - } - } - } - gameConfig.IsExists = isConfigExecutorFound - return gameConfig -} - -func (g *gameConfig) getOldResults(clientInfo tokentype.Info) (configResults types.GameConfigResults, ok bool) { - key := utils.GenerateRoomKey(clientInfo) - data, ok := g.storage.GetData(key) - if ok { - if configResults, ok = data.(types.GameConfigResults); ok { - return configResults, true - } - } - return types.GameConfigResults{ - GameID: clientInfo.GameID, - RoomID: clientInfo.RoomID, - Result: make(map[string]interface{}), - }, false -} - -func (g *gameConfig) serOldResults(clientInfo tokentype.Info, configResults types.GameConfigResults) { - key := utils.GenerateRoomKey(clientInfo) - g.storage.SetData(key, configResults) -} - -func getAllFunctions() []types.GameConfigExecutor { - return GetCountingFunctions() -} - -func NewGameConfigsService(storage memoryDB.IMemoryDB, token tokengenerator.TokenGenerator) GameConfigsService { - return &gameConfig{allGameConfigExecutor: getAllFunctions(), storage: storage, token: token} -} diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/numbers.go b/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/numbers.go deleted file mode 100644 index 34bf68a..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/internal/service/configs_service/numbers.go +++ /dev/null @@ -1,78 +0,0 @@ -package configsService - -func incrementValue(value any) any { - switch v := value.(type) { - case int: - return v + 1 - case int32: - return v + 1 - case int64: - return v + 1 - case float32: - return v + 1 - case float64: - return v + 1 - default: - return value - } -} - -func decrementValue(value any) any { - switch v := value.(type) { - case int: - return v - 1 - case int32: - return v - 1 - case int64: - return v - 1 - case float32: - return v - 1 - case float64: - return v - 1 - default: - return value - } -} - -func additionValues(v1, v2 any, expectedType string) any { - switch expectedType { - case "int": - return int(toFloat64(v1) + toFloat64(v2)) - case "int64": - return int64(toFloat64(v1) + toFloat64(v2)) - case "float64": - return toFloat64(v1) + toFloat64(v2) - default: - return nil // В случае неподдерживаемого типа - } -} - -func subtractValues(v1, v2 any, expectedType string) any { - switch expectedType { - case "int": - return int(toFloat64(v1) - toFloat64(v2)) - case "int64": - return int64(toFloat64(v1) + toFloat64(v2)) - case "float64": - return toFloat64(v1) - toFloat64(v2) - default: - return nil // В случае неподдерживаемого типа - } -} - -func toFloat64(value any) float64 { - switch v := value.(type) { - case int: - return float64(v) - case int32: - return float64(v) - case int64: - return float64(v) - case float32: - return float64(v) - case float64: - return v - default: - return 0 - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/service/service.go b/vendor/github.com/ascenmmo/tcp-server/internal/service/service.go index 7fa791d..e91627c 100644 --- a/vendor/github.com/ascenmmo/tcp-server/internal/service/service.go +++ b/vendor/github.com/ascenmmo/tcp-server/internal/service/service.go @@ -1,41 +1,32 @@ package service import ( - "fmt" - entities2 "github.com/ascenmmo/tcp-server/internal/entities" - configsService "github.com/ascenmmo/tcp-server/internal/service/configs_service" "github.com/ascenmmo/tcp-server/internal/storage" utils2 "github.com/ascenmmo/tcp-server/internal/utils" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/ascenmmo/tcp-server/pkg/errors" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" tokengenerator "github.com/ascenmmo/token-generator/token_generator" tokentype "github.com/ascenmmo/token-generator/token_type" "github.com/google/uuid" "github.com/rs/zerolog" - "runtime" "sync" "time" ) type Service interface { GetConnectionsNum() (countConn int, exists bool) - CreateRoom(token string, configs types.GameConfigs) (err error) + CreateRoom(token string) (err error) SetMessage(token string, req types.RequestSetMessage) (err error) GetMessages(token string) (msg types.ResponseGetMessage, err error) RemoveUser(userID uuid.UUID, reqToken string) (err error) - - SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) - NotifyAllServers(clientInfo tokentype.Info, req types.RequestSetMessage) (err error) - GetGameResults(token string) (results []types.GameConfigResults, err error) } type service struct { maxConnections uint64 - storage memoryDB.IMemoryDB - gameConfigService configsService.GameConfigsService + storage memoryDB.IMemoryDB token tokengenerator.TokenGenerator @@ -53,7 +44,7 @@ func (s *service) GetConnectionsNum() (countConn int, exists bool) { return count, true } -func (s *service) CreateRoom(token string, configs types.GameConfigs) error { +func (s *service) CreateRoom(token string) error { clientInfo, err := s.token.ParseToken(token) if err != nil { return err @@ -66,12 +57,9 @@ func (s *service) CreateRoom(token string, configs types.GameConfigs) error { return errors.ErrRoomIsExists } - configs = s.gameConfigService.SetServerExecuteToGameConfig(clientInfo, configs) - - s.setRoom(clientInfo, &entities2.Room{ - GameID: clientInfo.GameID, - RoomID: clientInfo.RoomID, - GameConfigs: configs, + s.setRoom(clientInfo, &types.Room{ + GameID: clientInfo.GameID, + RoomID: clientInfo.RoomID, }) return nil @@ -103,24 +91,13 @@ func (s *service) SetMessage(token string, msg types.RequestSetMessage) (err err } if !isFound { - room.SetUser(&entities2.User{ + room.SetUser(&types.User{ ID: clientInfo.UserID, }) } s.setRoom(clientInfo, room) - if msg.Server == nil { - s.gameConfigService.Do(token, clientInfo, room.GameConfigs, msg.Data) - id := uuid.New() - msg.Server = &id - msg.Token = token - err := s.NotifyAllServers(clientInfo, msg) - if err != nil { - s.logger.Warn().Err(err).Msg("failed to notify servers") - } - } - return nil } @@ -148,7 +125,7 @@ func (s *service) GetMessages(token string) (msg types.ResponseGetMessage, err e } if !isFound { - room.SetUser(&entities2.User{ + room.SetUser(&types.User{ ID: clientInfo.UserID, }) //s.setRoom(clientInfo, room) @@ -176,81 +153,7 @@ func (s *service) RemoveUser(userID uuid.UUID, reqToken string) (err error) { return nil } -func (s *service) SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) { - clientInfo, err := s.token.ParseToken(token) - if err != nil { - return err - } - - room, err := s.getRoom(clientInfo) - if err != nil { - return err - } - - room.SetServerID(id) - - data, _ := s.storage.GetData(utils2.GenerateNotifyServerKey()) - - server, ok := data.(entities2.NotifyServers) - if !ok { - s.logger.Warn().Msg("NotifyServers cant get interfase") - server = entities2.NewNotifierServers() - } - - err = server.AddServer(id, token, url) - if err != nil { - return err - } - - s.storage.SetData(utils2.GenerateNotifyServerKey(), server) - - return nil - -} - -func (s *service) NotifyAllServers(clientInfo tokentype.Info, req types.RequestSetMessage) (err error) { - room, err := s.getRoom(clientInfo) - if err != nil { - return err - } - if len(room.ServerID) == 0 { - return nil - } - - data, ok := s.storage.GetData(utils2.GenerateNotifyServerKey()) - if !ok { - return errors.ErrNotifyServerNotFound - } - - servers, ok := data.(entities2.NotifyServers) - if !ok { - return errors.ErrNotifyServerNotValid - } - - err = servers.NotifyServers(room.ServerID, req) - if err != nil { - return err - } - - return nil -} - -func (s *service) GetGameResults(token string) (results []types.GameConfigResults, err error) { - clientInfo, err := s.token.ParseToken(token) - if err != nil { - return results, err - } - - playersOnline := s.storage.GetAllConnection() - roomsResults, ok := s.gameConfigService.GetDeletedRoomsResults(clientInfo, playersOnline) - if !ok { - return results, errors.ErrGameResultsNotFound - } - - return roomsResults, nil -} - -func (s *service) getRoom(clientInfo tokentype.Info) (room *entities2.Room, err error) { +func (s *service) getRoom(clientInfo tokentype.Info) (room *types.Room, err error) { roomKey := utils2.GenerateRoomKey(clientInfo) roomData, ok := s.storage.GetData(roomKey) @@ -258,7 +161,7 @@ func (s *service) getRoom(clientInfo tokentype.Info) (room *entities2.Room, err return room, errors.ErrRoomNotFound } - room, ok = roomData.(*entities2.Room) + room, ok = roomData.(*types.Room) if !ok { return room, errors.ErrRoomBadValue } @@ -268,25 +171,17 @@ func (s *service) getRoom(clientInfo tokentype.Info) (room *entities2.Room, err return room, nil } -func (s *service) setRoom(clientInfo tokentype.Info, room *entities2.Room) { +func (s *service) setRoom(clientInfo tokentype.Info, room *types.Room) { roomKey := utils2.GenerateRoomKey(clientInfo) s.storage.SetData(roomKey, room) } -func NewService(token tokengenerator.TokenGenerator, storage memoryDB.IMemoryDB, gameConfigService configsService.GameConfigsService, logger zerolog.Logger) Service { +func NewService(token tokengenerator.TokenGenerator, storage memoryDB.IMemoryDB, logger zerolog.Logger) Service { srv := &service{ - maxConnections: uint64(types.CountConnectionsMAX()), - storage: storage, - token: token, - gameConfigService: gameConfigService, - logger: logger, + maxConnections: uint64(types.CountConnectionsMAX()), + storage: storage, + token: token, + logger: logger, } - go func() { - ticker := time.NewTicker(time.Second * 3) - for range ticker.C { - fmt.Println(fmt.Sprintf("count connections: %d \t max conections: %d", srv.storage.CountConnection(), srv.maxConnections)) - fmt.Println(fmt.Sprintf("count gorutines: %d ", runtime.NumGoroutine())) - } - }() return srv } diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/storage/memory_db.go b/vendor/github.com/ascenmmo/tcp-server/internal/storage/memory_db.go index ff323f9..0d52a2d 100644 --- a/vendor/github.com/ascenmmo/tcp-server/internal/storage/memory_db.go +++ b/vendor/github.com/ascenmmo/tcp-server/internal/storage/memory_db.go @@ -2,8 +2,6 @@ package memoryDB import ( "context" - "fmt" - "runtime" "sync" "time" ) @@ -40,16 +38,6 @@ type connections struct { count int } -func NewMemoryDb(ctx context.Context, dataTTL time.Duration) *MemoryDb { - db := &MemoryDb{ - userData: &userData{storage: sync.Map{}}, - connections: &connections{storage: sync.Map{}}, - dataTTL: dataTTL, - } - go db.Run(ctx) - return db -} - func (db *MemoryDb) GetData(key string) (any, bool) { value, ok := db.userData.storage.Load(key) if !ok { @@ -128,12 +116,14 @@ func (db *MemoryDb) removeOldData() { return true }) - db.logMemoryUsage() } -func (db *MemoryDb) logMemoryUsage() { - var stats runtime.MemStats - runtime.ReadMemStats(&stats) - fmt.Printf("Memory Usage: Alloc = %v MiB, TotalAlloc = %v MiB, Sys = %v MiB, NumGC = %v\n", - stats.Alloc/1024/1024, stats.TotalAlloc/1024/1024, stats.Sys/1024/1024, stats.NumGC) +func NewMemoryDb(ctx context.Context, dataTTL time.Duration) *MemoryDb { + db := &MemoryDb{ + userData: &userData{storage: sync.Map{}}, + connections: &connections{storage: sync.Map{}}, + dataTTL: dataTTL, + } + go db.Run(ctx) + return db } diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/connections.go b/vendor/github.com/ascenmmo/tcp-server/pkg/api/connections.go similarity index 92% rename from vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/connections.go rename to vendor/github.com/ascenmmo/tcp-server/pkg/api/connections.go index bf20a45..5f54982 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/connections.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/api/connections.go @@ -6,11 +6,11 @@ //go:generate tg transport --services . --out ../../pkg/transport --outSwagger ../../pkg/swagger.yaml //go:generate tg client -go --services . --outPath ../../pkg/clients/tcpGameServer -package restconnection +package api import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/google/uuid" ) diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/server_settings.go b/vendor/github.com/ascenmmo/tcp-server/pkg/api/server_settings.go similarity index 71% rename from vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/server_settings.go rename to vendor/github.com/ascenmmo/tcp-server/pkg/api/server_settings.go index 33f073b..70112fd 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/server_settings.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/api/server_settings.go @@ -6,12 +6,11 @@ //go:generate tg transport --services . --out ../../pkg/transport --outSwagger ../../pkg/swagger.yaml //go:generate tg client -go --services . --outPath ../../pkg/clients/tcpGameServer -package restconnection +package api import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/tcp-server/pkg/api/types" ) // @tg http-prefix=api/v1/rest/ @@ -30,10 +29,4 @@ type ServerSettings interface { // @tg http-headers=token|Token // @tg summary=`CreateRoom` CreateRoom(ctx context.Context, token string, createRoom types.CreateRoomRequest) (err error) - // @tg http-headers=token|Token - // @tg summary=`SetNotifyServer` - SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) - // @tg http-headers=token|Token - // @tg summary=`GetGameResults` - GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) } diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/api/types/network.go b/vendor/github.com/ascenmmo/tcp-server/pkg/api/types/network.go new file mode 100644 index 0000000..19b5f73 --- /dev/null +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/api/types/network.go @@ -0,0 +1,13 @@ +package types + +type RequestSetMessage struct { + Data any `json:"data"` +} + +type ResponseGetMessage struct { + DataArray []any `json:"dataArray"` +} + +type CreateRoomRequest struct { + TTL string `json:"time_to_live"` +} diff --git a/vendor/github.com/ascenmmo/tcp-server/internal/entities/game.go b/vendor/github.com/ascenmmo/tcp-server/pkg/api/types/room.go similarity index 93% rename from vendor/github.com/ascenmmo/tcp-server/internal/entities/game.go rename to vendor/github.com/ascenmmo/tcp-server/pkg/api/types/room.go index 180ceed..4416721 100644 --- a/vendor/github.com/ascenmmo/tcp-server/internal/entities/game.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/api/types/room.go @@ -1,7 +1,6 @@ -package entities +package types import ( - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" "github.com/google/uuid" "sync" "time" @@ -13,8 +12,6 @@ type Room struct { ServerID []uuid.UUID - GameConfigs types.GameConfigs - Users []*User UpdatedAt time.Time diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/settings.go b/vendor/github.com/ascenmmo/tcp-server/pkg/api/types/settings.go similarity index 100% rename from vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/settings.go rename to vendor/github.com/ascenmmo/tcp-server/pkg/api/types/settings.go diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/batch.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/batch.go deleted file mode 100644 index 98af465..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/batch.go +++ /dev/null @@ -1,32 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "context" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc" -) - -type RequestRPC struct { - retHandler rpcCallback - rpcRequest *jsonrpc.RequestRPC -} - -type rpcCallback func(err error, response *jsonrpc.ResponseRPC) - -func (cli *ClientJsonRPC) Batch(ctx context.Context, requests ...RequestRPC) { - - var rpcRequests jsonrpc.RequestsRPC - callbacks := make(map[jsonrpc.ID]rpcCallback) - for _, request := range requests { - rpcRequests = append(rpcRequests, request.rpcRequest) - callbacks[request.rpcRequest.ID] = request.retHandler - } - var err error - var rpcResponses jsonrpc.ResponsesRPC - rpcResponses, err = cli.rpc.CallBatch(ctx, rpcRequests) - for id, response := range rpcResponses.AsMap() { - if callback := callbacks[id]; callback != nil { - callback(err, response) - } - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cache.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cache.go deleted file mode 100644 index 4c78271..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cache.go +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "context" - "time" -) - -type cache interface { - SetTTL(ctx context.Context, key string, value interface{}, ttl time.Duration) (err error) - GetTTL(ctx context.Context, key string, value interface{}) (createdAt time.Time, ttl time.Duration, err error) -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/breaker.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/breaker.go deleted file mode 100644 index aefc642..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/breaker.go +++ /dev/null @@ -1,214 +0,0 @@ -package cb - -import ( - "sync" - "time" -) - -type CircuitBreaker struct { - name string - maxRequests uint32 - interval time.Duration - timeout time.Duration - readyToTrip func(counts Counts) bool - isSuccessful func(err error) bool - onStateChange func(name string, from State, to State) - - state State - generation uint64 - counts Counts - expiry time.Time - mutex sync.Mutex -} - -func NewCircuitBreaker(name string, st Settings) *CircuitBreaker { - - cb := new(CircuitBreaker) - cb.name = name - cb.onStateChange = st.OnStateChange - if st.MaxRequests == 0 { - cb.maxRequests = 1 - } else { - cb.maxRequests = st.MaxRequests - } - if st.Interval <= 0 { - cb.interval = defaultInterval - } else { - cb.interval = st.Interval - } - if st.Timeout <= 0 { - cb.timeout = defaultTimeout - } else { - cb.timeout = st.Timeout - } - if st.ReadyToTrip == nil { - cb.readyToTrip = defaultReadyToTrip - } else { - cb.readyToTrip = st.ReadyToTrip - } - if st.IsSuccessful == nil { - cb.isSuccessful = defaultIsSuccessful - } else { - cb.isSuccessful = st.IsSuccessful - } - cb.toNewGeneration(time.Now()) - return cb -} - -func (cb *CircuitBreaker) Name() string { - return cb.name -} - -func (cb *CircuitBreaker) State() State { - - cb.mutex.Lock() - defer cb.mutex.Unlock() - - now := time.Now() - state, _ := cb.currentState(now) - return state -} - -func (cb *CircuitBreaker) Counts() Counts { - - cb.mutex.Lock() - defer cb.mutex.Unlock() - - return cb.counts -} - -func (cb *CircuitBreaker) Execute(req func() error, opts ...Option) (err error) { - - var generation uint64 - values := prepareOpts(opts) - if generation, err = cb.beforeRequest(); err != nil { - if err == ErrOpenState && values.fallback != nil { - if fallBackErr := values.fallback(err); fallBackErr == nil { - return nil - } - } - return - } - defer func() { - e := recover() - if e != nil { - cb.afterRequest(generation, false) - panic(e) - } - }() - err = req() - isSuccessful := cb.isSuccessful - if values.isSuccessful != nil { - isSuccessful = values.isSuccessful - } - successful := isSuccessful(err) - if !successful && values.fallback != nil { - err = values.fallback(err) - } - cb.afterRequest(generation, successful) - return -} - -func (cb *CircuitBreaker) beforeRequest() (uint64, error) { - - cb.mutex.Lock() - defer cb.mutex.Unlock() - now := time.Now() - state, generation := cb.currentState(now) - if state == StateOpen { - return generation, ErrOpenState - } else if state == StateHalfOpen && cb.counts.Requests >= cb.maxRequests { - return generation, ErrTooManyRequests - } - cb.counts.onRequest() - return generation, nil -} - -func (cb *CircuitBreaker) afterRequest(before uint64, success bool) { - - cb.mutex.Lock() - defer cb.mutex.Unlock() - now := time.Now() - state, generation := cb.currentState(now) - if generation != before { - return - } - if success { - cb.onSuccess(state, now) - } else { - cb.onFailure(state, now) - } -} - -func (cb *CircuitBreaker) onSuccess(state State, now time.Time) { - - switch state { - case StateClosed: - cb.counts.onSuccess() - case StateHalfOpen: - cb.counts.onSuccess() - if cb.counts.ConsecutiveSuccesses >= cb.maxRequests { - cb.setState(StateClosed, now) - } - } -} - -func (cb *CircuitBreaker) onFailure(state State, now time.Time) { - - switch state { - case StateClosed: - cb.counts.onFailure() - if cb.readyToTrip(cb.counts) { - cb.setState(StateOpen, now) - } - case StateHalfOpen: - cb.setState(StateOpen, now) - } -} - -func (cb *CircuitBreaker) currentState(now time.Time) (State, uint64) { - - switch cb.state { - case StateClosed: - if !cb.expiry.IsZero() && cb.expiry.Before(now) { - cb.toNewGeneration(now) - } - case StateOpen: - if cb.expiry.Before(now) { - cb.setState(StateHalfOpen, now) - } - } - return cb.state, cb.generation -} - -func (cb *CircuitBreaker) setState(state State, now time.Time) { - - if cb.state == state { - return - } - prev := cb.state - cb.state = state - cb.toNewGeneration(now) - if cb.onStateChange != nil { - cb.onStateChange(cb.name, prev, state) - } -} - -func (cb *CircuitBreaker) toNewGeneration(now time.Time) { - - cb.generation++ - cb.counts.clear() - var zero time.Time - switch cb.state { - case StateClosed: - if cb.interval == 0 { - cb.expiry = zero - } else { - cb.expiry = now.Add(cb.interval) - } - case StateOpen: - cb.expiry = now.Add(cb.timeout) - default: - cb.expiry = zero - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/count.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/count.go deleted file mode 100644 index bdf1dc6..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/count.go +++ /dev/null @@ -1,36 +0,0 @@ -package cb - -type Counts struct { - Requests uint32 - TotalSuccesses uint32 - TotalFailures uint32 - ConsecutiveSuccesses uint32 - ConsecutiveFailures uint32 -} - -func (c *Counts) onRequest() { - c.Requests++ -} - -func (c *Counts) onSuccess() { - - c.TotalSuccesses++ - c.ConsecutiveSuccesses++ - c.ConsecutiveFailures = 0 -} - -func (c *Counts) onFailure() { - - c.TotalFailures++ - c.ConsecutiveFailures++ - c.ConsecutiveSuccesses = 0 -} - -func (c *Counts) clear() { - - c.Requests = 0 - c.TotalSuccesses = 0 - c.TotalFailures = 0 - c.ConsecutiveSuccesses = 0 - c.ConsecutiveFailures = 0 -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/default.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/default.go deleted file mode 100644 index 1a9aaa5..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/default.go +++ /dev/null @@ -1,16 +0,0 @@ -package cb - -import ( - "time" -) - -const defaultInterval = time.Duration(0) * time.Second -const defaultTimeout = time.Duration(10) * time.Second - -func defaultReadyToTrip(counts Counts) bool { - return counts.ConsecutiveFailures >= 5 -} - -func defaultIsSuccessful(err error) bool { - return err == nil -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/error.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/error.go deleted file mode 100644 index 31a6004..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/error.go +++ /dev/null @@ -1,10 +0,0 @@ -package cb - -import ( - "errors" -) - -var ( - ErrTooManyRequests = errors.New("too many requests") - ErrOpenState = errors.New("circuit breaker is open") -) diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/option.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/option.go deleted file mode 100644 index 3c3d965..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/option.go +++ /dev/null @@ -1,28 +0,0 @@ -package cb - -type Option func(ops *options) - -type options struct { - fallback func(err error) error - isSuccessful func(err error) (success bool) -} - -func prepareOpts(opts []Option) (options options) { - - for _, op := range opts { - op(&options) - } - return -} - -func IsSuccessful(isSuccessful func(err error) (success bool)) Option { - return func(ops *options) { - ops.isSuccessful = isSuccessful - } -} - -func Fallback(fallback func(err error) error) Option { - return func(ops *options) { - ops.fallback = fallback - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/setting.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/setting.go deleted file mode 100644 index 2fd9ce0..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/setting.go +++ /dev/null @@ -1,14 +0,0 @@ -package cb - -import ( - "time" -) - -type Settings struct { - MaxRequests uint32 - Interval time.Duration - Timeout time.Duration - ReadyToTrip func(counts Counts) bool - OnStateChange func(name string, from State, to State) - IsSuccessful func(err error) bool -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/state.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/state.go deleted file mode 100644 index 4037ff1..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/state.go +++ /dev/null @@ -1,27 +0,0 @@ -package cb - -import ( - "fmt" -) - -type State int - -const ( - StateClosed State = iota - StateHalfOpen - StateOpen -) - -func (s State) String() string { - - switch s { - case StateClosed: - return "closed" - case StateHalfOpen: - return "half-open" - case StateOpen: - return "open" - default: - return fmt.Sprintf("unknown state: %d", s) - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/tscb.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/tscb.go deleted file mode 100644 index 6b049d8..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb/tscb.go +++ /dev/null @@ -1,35 +0,0 @@ -package cb - -type TwoStepCircuitBreaker struct { - cb *CircuitBreaker -} - -func NewTwoStepCircuitBreaker(name string, st Settings) *TwoStepCircuitBreaker { - return &TwoStepCircuitBreaker{ - cb: NewCircuitBreaker(name, st), - } -} - -func (cbTs *TwoStepCircuitBreaker) Name() string { - return cbTs.cb.Name() -} - -func (cbTs *TwoStepCircuitBreaker) State() State { - return cbTs.cb.State() -} - -func (cbTs *TwoStepCircuitBreaker) Counts() Counts { - return cbTs.cb.Counts() -} - -func (cbTs *TwoStepCircuitBreaker) Allow() (done func(success bool), err error) { - - var generation uint64 - if generation, err = cbTs.cb.beforeRequest(); err != nil { - return - } - done = func(success bool) { - cbTs.cb.afterRequest(generation, success) - } - return -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/error.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/error.go deleted file mode 100644 index 4cdb65f..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/error.go +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import "encoding/json" - -type errorJsonRPC struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} - -func (err errorJsonRPC) Error() string { - return err.Message -} - -type ErrorDecoder func(errData json.RawMessage) error - -func defaultErrorDecoder(errData json.RawMessage) (err error) { - - var jsonrpcError errorJsonRPC - if err = json.Unmarshal(errData, &jsonrpcError); err != nil { - return - } - return jsonrpcError -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-exchange.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-exchange.go deleted file mode 100644 index 1238961..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-exchange.go +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" -) - -type requestGameConnectionsSetSendMessage struct { - Token string `json:"token"` - Message types.RequestSetMessage `json:"message"` -} - -// Formal exchange type, please do not delete. -type responseGameConnectionsSetSendMessage struct{} - -type requestGameConnectionsGetMessage struct { - Token string `json:"token"` -} - -type responseGameConnectionsGetMessage struct { - Messages types.ResponseGetMessage `json:"messages"` -} - -type requestGameConnectionsRemoveUser struct { - Token string `json:"token"` - UserID uuid.UUID `json:"userID"` -} - -// Formal exchange type, please do not delete. -type responseGameConnectionsRemoveUser struct{} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-fallback.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-fallback.go deleted file mode 100644 index 8a82fa3..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-fallback.go +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -type fallbackGameConnections interface { - SetSendMessage(err error) bool - GetMessage(err error) bool - RemoveUser(err error) bool -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-jsonrpc.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-jsonrpc.go deleted file mode 100644 index a81522a..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/gameconnections-jsonrpc.go +++ /dev/null @@ -1,190 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "context" - "fmt" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" -) - -type ClientGameConnections struct { - *ClientJsonRPC -} - -type retGameConnectionsSetSendMessage = func(err error) -type retGameConnectionsGetMessage = func(messages types.ResponseGetMessage, err error) -type retGameConnectionsRemoveUser = func(err error) - -func (cli *ClientGameConnections) SetSendMessage(ctx context.Context, token string, message types.RequestSetMessage) (err error) { - - request := requestGameConnectionsSetSendMessage{ - Message: message, - Token: token, - } - var response responseGameConnectionsSetSendMessage - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "gameconnections.setsendmessage", request) - var fallbackCheck func(error) bool - if cli.fallbackGameConnections != nil { - fallbackCheck = cli.fallbackGameConnections.SetSendMessage - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return err -} - -func (cli *ClientGameConnections) ReqSetSendMessage(ctx context.Context, callback retGameConnectionsSetSendMessage, token string, message types.RequestSetMessage) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "gameconnections.setsendmessage", - Params: requestGameConnectionsSetSendMessage{ - Message: message, - Token: token, - }, - }} - if callback != nil { - var response responseGameConnectionsSetSendMessage - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackGameConnections != nil { - fallbackCheck = cli.fallbackGameConnections.SetSendMessage - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - -func (cli *ClientGameConnections) GetMessage(ctx context.Context, token string) (messages types.ResponseGetMessage, err error) { - - request := requestGameConnectionsGetMessage{Token: token} - var response responseGameConnectionsGetMessage - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "gameconnections.getmessage", request) - var fallbackCheck func(error) bool - if cli.fallbackGameConnections != nil { - fallbackCheck = cli.fallbackGameConnections.GetMessage - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return response.Messages, err -} - -func (cli *ClientGameConnections) ReqGetMessage(ctx context.Context, callback retGameConnectionsGetMessage, token string) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "gameconnections.getmessage", - Params: requestGameConnectionsGetMessage{Token: token}, - }} - if callback != nil { - var response responseGameConnectionsGetMessage - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackGameConnections != nil { - fallbackCheck = cli.fallbackGameConnections.GetMessage - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(response.Messages, cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - -func (cli *ClientGameConnections) RemoveUser(ctx context.Context, token string, userID uuid.UUID) (err error) { - - request := requestGameConnectionsRemoveUser{ - Token: token, - UserID: userID, - } - var response responseGameConnectionsRemoveUser - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "gameconnections.removeuser", request) - var fallbackCheck func(error) bool - if cli.fallbackGameConnections != nil { - fallbackCheck = cli.fallbackGameConnections.RemoveUser - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return err -} - -func (cli *ClientGameConnections) ReqRemoveUser(ctx context.Context, callback retGameConnectionsRemoveUser, token string, userID uuid.UUID) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "gameconnections.removeuser", - Params: requestGameConnectionsRemoveUser{ - Token: token, - UserID: userID, - }, - }} - if callback != nil { - var response responseGameConnectionsRemoveUser - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackGameConnections != nil { - fallbackCheck = cli.fallbackGameConnections.RemoveUser - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/errors.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/errors.go deleted file mode 100644 index 8ad18fc..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package hasher - -import ( - "fmt" -) - -type ErrNotStringer struct { - Field string -} - -func (ens *ErrNotStringer) Error() string { - return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field) -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/hasher.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/hasher.go deleted file mode 100644 index 632d6b9..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/hasher.go +++ /dev/null @@ -1,28 +0,0 @@ -package hasher - -import ( - "hash/fnv" - "reflect" - "time" -) - -const ( - hashTag = "hash" -) - -var timeType = reflect.TypeOf(time.Time{}) - -func Hash(v interface{}, opts ...Option) (hash uint64, err error) { - - values := prepareOpts(opts) - w := &walker{ - tag: hashTag, - h: fnv.New64(), - zeroNil: values.zeroNil, - stringer: values.useStringer, - sets: values.slicesAsSets, - ignoreZeroValue: values.ignoreZeroValue, - } - w.h.Reset() - return w.visit(reflect.ValueOf(v), nil) -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/include.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/include.go deleted file mode 100644 index d780798..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/include.go +++ /dev/null @@ -1,13 +0,0 @@ -package hasher - -type Hasher interface { - Hash() (hash uint64, err error) -} - -type Inc interface { - HashInclude(field string, v interface{}) (has bool, err error) -} - -type IncMap interface { - HashIncludeMap(field string, k, v interface{}) (has bool, err error) -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/option.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/option.go deleted file mode 100644 index 50fee42..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/option.go +++ /dev/null @@ -1,42 +0,0 @@ -package hasher - -type options struct { - zeroNil bool - useStringer bool - slicesAsSets bool - ignoreZeroValue bool -} - -type Option func(ops *options) - -func prepareOpts(opts []Option) (options options) { - - for _, op := range opts { - op(&options) - } - return -} - -func ZeroNil() Option { - return func(ops *options) { - ops.zeroNil = true - } -} - -func UseStringer() Option { - return func(ops *options) { - ops.useStringer = true - } -} - -func SlicesAsSets() Option { - return func(ops *options) { - ops.slicesAsSets = true - } -} - -func IgnoreZeroValue() Option { - return func(ops *options) { - ops.ignoreZeroValue = true - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/walker.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/walker.go deleted file mode 100644 index f4c7d28..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher/walker.go +++ /dev/null @@ -1,261 +0,0 @@ -package hasher - -import ( - "encoding/binary" - "fmt" - "hash" - "reflect" - "time" -) - -type visitFlag uint - -const ( - visitFlagSet = iota << 1 -) - -type walker struct { - h hash.Hash64 - tag string - sets bool - zeroNil bool - stringer bool - ignoreZeroValue bool -} - -type visitOpts struct { - Flags visitFlag - Struct interface{} - StructField string -} - -func (w *walker) visit(v reflect.Value, opts *visitOpts) (hash uint64, err error) { - - t := reflect.TypeOf(0) - for { - if v.Kind() == reflect.Interface { - v = v.Elem() - continue - } - if v.Kind() == reflect.Ptr { - if w.zeroNil { - t = v.Type().Elem() - } - v = reflect.Indirect(v) - continue - } - break - } - if !v.IsValid() { - v = reflect.Zero(t) - } - switch v.Kind() { - case reflect.Int: - v = reflect.ValueOf(v.Int()) - case reflect.Uint: - v = reflect.ValueOf(v.Uint()) - case reflect.Bool: - var tmp int8 - if v.Bool() { - tmp = 1 - } - v = reflect.ValueOf(tmp) - } - k := v.Kind() - if k >= reflect.Int && k <= reflect.Complex64 { - w.h.Reset() - _ = binary.Write(w.h, binary.LittleEndian, v.Interface()) - return w.h.Sum64(), err - } - switch v.Type() { - case timeType: - w.h.Reset() - b, err := v.Interface().(time.Time).MarshalBinary() - if err != nil { - return 0, err - } - err = binary.Write(w.h, binary.LittleEndian, b) - return w.h.Sum64(), err - } - switch k { - case reflect.Array: - var h uint64 - l := v.Len() - for i := 0; i < l; i++ { - current, err := w.visit(v.Index(i), nil) - if err != nil { - return 0, err - } - h = hashUpdateOrdered(w.h, h, current) - } - return h, nil - case reflect.Map: - var includeMap IncMap - if opts != nil && opts.Struct != nil { - if v, ok := opts.Struct.(IncMap); ok { - includeMap = v - } - } - var h uint64 - for _, k := range v.MapKeys() { - v := v.MapIndex(k) - if includeMap != nil { - incl, err := includeMap.HashIncludeMap( - opts.StructField, k.Interface(), v.Interface()) - if err != nil { - return 0, err - } - if !incl { - continue - } - } - kh, err := w.visit(k, nil) - if err != nil { - return 0, err - } - vh, err := w.visit(v, nil) - if err != nil { - return 0, err - } - fieldHash := hashUpdateOrdered(w.h, kh, vh) - h = hashUpdateUnordered(h, fieldHash) - } - h = hashFinishUnordered(w.h, h) - return h, nil - case reflect.Struct: - parent := v.Interface() - var include Inc - if impl, ok := parent.(Inc); ok { - include = impl - } - if impl, ok := parent.(Hasher); ok { - return impl.Hash() - } - if v.CanAddr() { - vptr := v.Addr() - parentptr := vptr.Interface() - if impl, ok := parentptr.(Inc); ok { - include = impl - } - if impl, ok := parentptr.(Hasher); ok { - return impl.Hash() - } - } - t := v.Type() - h, err := w.visit(reflect.ValueOf(t.Name()), nil) - if err != nil { - return 0, err - } - l := v.NumField() - for i := 0; i < l; i++ { - if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { - var f visitFlag - fieldType := t.Field(i) - if fieldType.PkgPath != "" { - continue - } - tag := fieldType.Tag.Get(w.tag) - if tag == "ignore" || tag == "-" { - continue - } - if w.ignoreZeroValue { - if innerV.IsZero() { - continue - } - } - if tag == "string" || w.stringer { - if impl, ok := innerV.Interface().(fmt.Stringer); ok { - innerV = reflect.ValueOf(impl.String()) - } else if tag == "string" { - return 0, &ErrNotStringer{ - Field: v.Type().Field(i).Name, - } - } - } - if include != nil { - incl, err := include.HashInclude(fieldType.Name, innerV) - if err != nil { - return 0, err - } - if !incl { - continue - } - } - switch tag { - case "set": - f |= visitFlagSet - } - kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil) - if err != nil { - return 0, err - } - vh, err := w.visit(innerV, &visitOpts{ - Flags: f, - Struct: parent, - StructField: fieldType.Name, - }) - if err != nil { - return 0, err - } - fieldHash := hashUpdateOrdered(w.h, kh, vh) - h = hashUpdateUnordered(h, fieldHash) - } - h = hashFinishUnordered(w.h, h) - } - return h, nil - case reflect.Slice: - var h uint64 - var set bool - if opts != nil { - set = (opts.Flags & visitFlagSet) != 0 - } - l := v.Len() - for i := 0; i < l; i++ { - current, err := w.visit(v.Index(i), nil) - if err != nil { - return 0, err - } - if set || w.sets { - h = hashUpdateUnordered(h, current) - } else { - h = hashUpdateOrdered(w.h, h, current) - } - } - h = hashFinishUnordered(w.h, h) - return h, nil - case reflect.String: - w.h.Reset() - _, err := w.h.Write([]byte(v.String())) - return w.h.Sum64(), err - default: - return 0, fmt.Errorf("unknown kind to hash: %s", k) - } -} - -func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { - - h.Reset() - e1 := binary.Write(h, binary.LittleEndian, a) - e2 := binary.Write(h, binary.LittleEndian, b) - if e1 != nil { - panic(e1) - } - if e2 != nil { - panic(e2) - } - return h.Sum64() -} - -func hashUpdateUnordered(a, b uint64) uint64 { - return a ^ b -} - -func hashFinishUnordered(h hash.Hash64, a uint64) uint64 { - - h.Reset() - e1 := binary.Write(h, binary.LittleEndian, a) - if e1 != nil { - panic(e1) - } - return h.Sum64() -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc.go deleted file mode 100644 index a02f395..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc.go +++ /dev/null @@ -1,77 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "context" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc" - "os" - "strconv" - "time" -) - -type ClientJsonRPC struct { - name string - - rpc *jsonrpc.ClientRPC - rpcOpts []jsonrpc.Option - - cache cache - - cbCfg cb.Settings - cb *cb.CircuitBreaker - - fallbackTTL time.Duration - fallbackGameConnections fallbackGameConnections - fallbackServerSettings fallbackServerSettings - - errorDecoder ErrorDecoder -} - -func New(endpoint string, opts ...Option) (cli *ClientJsonRPC) { - - hostname, _ := os.Hostname() - cli = &ClientJsonRPC{ - errorDecoder: defaultErrorDecoder, - fallbackTTL: time.Hour * 24, - name: hostname + "_" + "github.com/ascenmmo/tcp-server", - } - cli.applyOpts(opts) - cli.rpc = jsonrpc.NewClient(endpoint, cli.rpcOpts...) - cli.cb = cb.NewCircuitBreaker("github.com/ascenmmo/tcp-server", cli.cbCfg) - return -} - -func (cli *ClientJsonRPC) GameConnections() *ClientGameConnections { - return &ClientGameConnections{ClientJsonRPC: cli} -} - -func (cli *ClientJsonRPC) ServerSettings() *ClientServerSettings { - return &ClientServerSettings{ClientJsonRPC: cli} -} - -func (cli *ClientJsonRPC) proceedResponse(ctx context.Context, httpErr error, cacheKey uint64, fallbackCheck func(error) bool, rpcResponse *jsonrpc.ResponseRPC, methodResponse interface{}) (err error) { - - err = cli.cb.Execute(func() (err error) { - if httpErr != nil { - return httpErr - } - return rpcResponse.GetObject(&methodResponse) - }, cb.IsSuccessful(func(err error) (success bool) { - if fallbackCheck != nil { - return fallbackCheck(err) - } - if success = err == nil; success { - if cli.cache != nil && cacheKey != 0 { - _ = cli.cache.SetTTL(ctx, strconv.FormatUint(cacheKey, 10), methodResponse, cli.fallbackTTL) - } - } - return - }), cb.Fallback(func(err error) error { - if cli.cache != nil && cacheKey != 0 { - _, _, err = cli.cache.GetTTL(ctx, strconv.FormatUint(cacheKey, 10), &methodResponse) - } - return err - })) - return -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/client.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/client.go deleted file mode 100644 index 07aca99..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/client.go +++ /dev/null @@ -1,38 +0,0 @@ -package jsonrpc - -import ( - "crypto/tls" - "net/http" - - "github.com/google/uuid" -) - -const ( - Version = "2.0" -) - -type ID = uuid.UUID - -var NilID = uuid.Nil -var NewID = uuid.New - -type ClientRPC struct { - options options - endpoint string - httpClient *http.Client -} - -func NewClient(endpoint string, opts ...Option) (client *ClientRPC) { - - client = &ClientRPC{ - endpoint: endpoint, - httpClient: &http.Client{}, - options: prepareOpts(opts), - } - if client.options.insecure { - client.httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - return client -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/error.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/error.go deleted file mode 100644 index 3e10420..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/error.go +++ /dev/null @@ -1,30 +0,0 @@ -package jsonrpc - -import ( - "encoding/json" - "strconv" -) - -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data json.RawMessage `json:"data,omitempty"` -} - -func (e *RPCError) Raw() (data json.RawMessage) { - data, _ = json.Marshal(e) - return -} - -func (e *RPCError) Error() string { - return strconv.Itoa(e.Code) + ": " + e.Message -} - -type HTTPError struct { - Code int - err error -} - -func (e *HTTPError) Error() string { - return e.err.Error() -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/http2curl.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/http2curl.go deleted file mode 100644 index fd72e63..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/http2curl.go +++ /dev/null @@ -1,58 +0,0 @@ -package jsonrpc - -import ( - "bytes" - "fmt" - "io" - "net/http" - "sort" - "strings" -) - -type CurlCommand struct { - slice []string -} - -type nopCloser struct { - io.Reader -} - -func toCurl(req *http.Request) (command *CurlCommand, err error) { - - command = &CurlCommand{} - command.append("curl") - command.append("-X", bashEscape(req.Method)) - if req.Body != nil { - var body []byte - if body, err = io.ReadAll(req.Body); err != nil { - return - } - req.Body = nopCloser{bytes.NewBuffer(body)} - bodyEscaped := bashEscape(string(body)) - command.append("-d", bodyEscaped) - } - var keys []string - for k := range req.Header { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - command.append("-H", bashEscape(fmt.Sprintf("%s: %s", k, strings.Join(req.Header[k], " ")))) - } - command.append(bashEscape(req.URL.String())) - return -} - -func (c *CurlCommand) append(newSlice ...string) { - c.slice = append(c.slice, newSlice...) -} - -func (c *CurlCommand) String() string { - return strings.Join(c.slice, " ") -} - -func bashEscape(str string) string { - return `'` + strings.Replace(str, `'`, `'\''`, -1) + `'` -} - -func (nopCloser) Close() error { return nil } diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/internal.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/internal.go deleted file mode 100644 index 68fb4b2..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/internal.go +++ /dev/null @@ -1,182 +0,0 @@ -package jsonrpc - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/rs/zerolog/log" -) - -func (client *ClientRPC) newRequest(ctx context.Context, reqBody interface{}) (request *http.Request, err error) { - - var body []byte - if body, err = json.Marshal(reqBody); err != nil { - return - } - if request, err = http.NewRequestWithContext(ctx, "POST", client.endpoint, bytes.NewReader(body)); err != nil { - return - } - request.Header.Set("Accept", "application/json") - request.Header.Set("Content-Type", "application/json") - for k, v := range client.options.customHeaders { - if k == "Host" { - request.Host = v - } else { - request.Header.Set(k, v) - } - } - for _, header := range client.options.headersFromCtx { - if value := ctx.Value(header); value != nil { - if k := toString(header); k != "" { - if v := toString(value); v != "" { - request.Header.Set(k, v) - } - } - } - } - return -} - -func (client *ClientRPC) doCall(ctx context.Context, request *RequestRPC) (rpcResponse *ResponseRPC, err error) { - - var httpRequest *http.Request - if httpRequest, err = client.newRequest(ctx, request); err != nil { - err = fmt.Errorf("rpc call %v() on %v: %v", request.Method, client.endpoint, err.Error()) - return - } - if client.options.logRequests { - if cmd, cmdErr := toCurl(httpRequest); cmdErr == nil { - log.Ctx(ctx).Debug().Str("method", request.Method).Str("curl", cmd.String()).Msg("call") - } - } - defer func() { - if err != nil && client.options.logOnError { - if cmd, cmdErr := toCurl(httpRequest); cmdErr == nil { - log.Ctx(ctx).Error().Str("method", request.Method).Str("curl", cmd.String()).Msg("call") - } - } - }() - var httpResponse *http.Response - if httpResponse, err = client.httpClient.Do(httpRequest); err != nil { - err = fmt.Errorf("rpc call %v() on %v: %v", request.Method, httpRequest.URL.String(), err.Error()) - return - } - defer httpResponse.Body.Close() - decoder := json.NewDecoder(httpResponse.Body) - if !client.options.allowUnknownFields { - decoder.DisallowUnknownFields() - } - decoder.UseNumber() - err = decoder.Decode(&rpcResponse) - if err != nil { - if httpResponse.StatusCode >= 400 { - return nil, &HTTPError{ - Code: httpResponse.StatusCode, - err: fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %v", request.Method, httpRequest.URL.String(), httpResponse.StatusCode, err.Error()), - } - } - return nil, fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %v", request.Method, httpRequest.URL.String(), httpResponse.StatusCode, err.Error()) - } - if rpcResponse == nil { - if httpResponse.StatusCode >= 400 { - return nil, &HTTPError{ - Code: httpResponse.StatusCode, - err: fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", request.Method, httpRequest.URL.String(), httpResponse.StatusCode), - } - } - err = fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", request.Method, httpRequest.URL.String(), httpResponse.StatusCode) - return - } - if httpResponse.StatusCode >= 400 { - if rpcResponse.Error != nil { - return rpcResponse, &HTTPError{ - Code: httpResponse.StatusCode, - err: fmt.Errorf("rpc call %v() on %v status code: %v. rpc response error: %v", request.Method, httpRequest.URL.String(), httpResponse.StatusCode, rpcResponse.Error), - } - } - return rpcResponse, &HTTPError{ - Code: httpResponse.StatusCode, - err: fmt.Errorf("rpc call %v() on %v status code: %v. no rpc error available", request.Method, httpRequest.URL.String(), httpResponse.StatusCode), - } - } - return -} - -func (client *ClientRPC) doBatchCall(ctx context.Context, rpcRequests []*RequestRPC) (rpcResponses ResponsesRPC, err error) { - - defer func() { - if err != nil { - for _, request := range rpcRequests { - if request.ID == NilID { - continue - } - rpcResponses = append(rpcResponses, &ResponseRPC{ - ID: request.ID, - JSONRPC: request.JSONRPC, - Error: &RPCError{ - Message: err.Error(), - }, - }) - } - } - }() - var httpRequest *http.Request - if httpRequest, err = client.newRequest(ctx, rpcRequests); err != nil { - err = fmt.Errorf("rpc batch call on %v: %v", client.endpoint, err.Error()) - return - } - if client.options.logRequests { - if cmd, cmdErr := toCurl(httpRequest); cmdErr == nil { - log.Ctx(ctx).Debug().Str("method", "batch").Int("count", len(rpcRequests)).Str("curl", cmd.String()).Msg("call") - } - } - defer func() { - if err != nil && client.options.logOnError { - if cmd, cmdErr := toCurl(httpRequest); cmdErr == nil { - log.Ctx(ctx).Error().Str("method", "batch").Int("count", len(rpcRequests)).Str("curl", cmd.String()).Msg("call") - } - } - }() - var httpResponse *http.Response - if httpResponse, err = client.httpClient.Do(httpRequest); err != nil { - err = fmt.Errorf("rpc batch call on %v: %v", httpRequest.URL.String(), err.Error()) - return - } - defer httpResponse.Body.Close() - decoder := json.NewDecoder(httpResponse.Body) - if !client.options.allowUnknownFields { - decoder.DisallowUnknownFields() - } - decoder.UseNumber() - err = decoder.Decode(&rpcResponses) - if err != nil { - if httpResponse.StatusCode >= 400 { - return nil, &HTTPError{ - Code: httpResponse.StatusCode, - err: fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %v", httpRequest.URL.String(), httpResponse.StatusCode, err.Error()), - } - } - err = fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %v", httpRequest.URL.String(), httpResponse.StatusCode, err.Error()) - return - } - if len(rpcResponses) == 0 { - if httpResponse.StatusCode >= 400 { - return nil, &HTTPError{ - Code: httpResponse.StatusCode, - err: fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode), - } - } - err = fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode) - return - } - if httpResponse.StatusCode >= 400 { - return rpcResponses, &HTTPError{ - Code: httpResponse.StatusCode, - err: fmt.Errorf("rpc batch call on %v status code: %v. check rpc responses for potential rpc error", httpRequest.URL.String(), httpResponse.StatusCode), - } - } - return -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/option.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/option.go deleted file mode 100644 index 79e5355..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/option.go +++ /dev/null @@ -1,51 +0,0 @@ -package jsonrpc - -type options struct { - insecure bool - logOnError bool - logRequests bool - allowUnknownFields bool - headersFromCtx []interface{} - customHeaders map[string]string -} - -type Option func(ops *options) - -func prepareOpts(opts []Option) (options options) { - - options.customHeaders = make(map[string]string) - for _, op := range opts { - op(&options) - } - return -} - -func HeaderFromCtx(headers ...interface{}) Option { - return func(ops *options) { - ops.headersFromCtx = headers - } -} - -func AllowUnknownFields(allowUnknownFields bool) Option { - return func(ops *options) { - ops.allowUnknownFields = allowUnknownFields - } -} - -func Insecure() Option { - return func(ops *options) { - ops.insecure = true - } -} - -func LogRequest() Option { - return func(ops *options) { - ops.logRequests = true - } -} - -func LogOnError() Option { - return func(ops *options) { - ops.logOnError = true - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/param.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/param.go deleted file mode 100644 index ca6b34a..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/param.go +++ /dev/null @@ -1,42 +0,0 @@ -package jsonrpc - -import ( - "reflect" -) - -func Params(params ...interface{}) interface{} { - - var finalParams interface{} - if params != nil { - switch len(params) { - case 0: - case 1: - if params[0] != nil { - var typeOf reflect.Type - for typeOf = reflect.TypeOf(params[0]); typeOf != nil && typeOf.Kind() == reflect.Ptr; typeOf = typeOf.Elem() { - } - if typeOf != nil { - switch typeOf.Kind() { - case reflect.Struct: - finalParams = params[0] - case reflect.Array: - finalParams = params[0] - case reflect.Slice: - finalParams = params[0] - case reflect.Interface: - finalParams = params[0] - case reflect.Map: - finalParams = params[0] - default: - finalParams = params - } - } - } else { - finalParams = params - } - default: - finalParams = params - } - } - return finalParams -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/public.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/public.go deleted file mode 100644 index d393b3e..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/public.go +++ /dev/null @@ -1,53 +0,0 @@ -package jsonrpc - -import ( - "context" - "errors" - - "github.com/google/uuid" -) - -func (client *ClientRPC) Call(ctx context.Context, method string, params ...interface{}) (response *ResponseRPC, err error) { - - request := &RequestRPC{ - ID: uuid.New(), - Method: method, - Params: Params(params...), - JSONRPC: Version, - } - return client.doCall(ctx, request) -} - -func (client *ClientRPC) CallRaw(ctx context.Context, request *RequestRPC) (response *ResponseRPC, err error) { - return client.doCall(ctx, request) -} - -func (client *ClientRPC) CallFor(ctx context.Context, out interface{}, method string, params ...interface{}) (err error) { - - rpcResponse, err := client.Call(ctx, method, params...) - if err != nil { - return err - } - if rpcResponse.Error != nil { - return rpcResponse.Error - } - return rpcResponse.GetObject(out) -} - -func (client *ClientRPC) CallBatch(ctx context.Context, requests RequestsRPC) (responses ResponsesRPC, err error) { - - if len(requests) == 0 { - err = errors.New("empty request list") - return - } - return client.doBatchCall(ctx, requests) -} - -func (client *ClientRPC) CallBatchRaw(ctx context.Context, requests RequestsRPC) (responses ResponsesRPC, err error) { - - if len(requests) == 0 { - err = errors.New("empty request list") - return - } - return client.doBatchCall(ctx, requests) -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/request.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/request.go deleted file mode 100644 index 7c33ef3..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/request.go +++ /dev/null @@ -1,36 +0,0 @@ -package jsonrpc - -import ( - "github.com/google/uuid" -) - -type RequestRPC struct { - ID ID `json:"id"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` - JSONRPC string `json:"jsonrpc"` -} - -type RequestsRPC []*RequestRPC - -func NewRequest(method string, params ...interface{}) *RequestRPC { - - request := &RequestRPC{ - ID: uuid.New(), - Method: method, - Params: Params(params...), - JSONRPC: Version, - } - return request -} - -func NewRequestWithID(id ID, method string, params ...interface{}) *RequestRPC { - - request := &RequestRPC{ - ID: id, - Method: method, - Params: Params(params...), - JSONRPC: Version, - } - return request -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/response.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/response.go deleted file mode 100644 index 5742016..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/response.go +++ /dev/null @@ -1,58 +0,0 @@ -package jsonrpc - -import ( - "encoding/json" - - "github.com/google/uuid" -) - -type ResponseRPC struct { - ID uuid.UUID `json:"id"` - JSONRPC string `json:"jsonrpc"` - Error *RPCError `json:"error,omitempty"` - Result json.RawMessage `json:"result,omitempty"` -} - -type ResponsesRPC []*ResponseRPC - -func (res ResponsesRPC) AsMap() map[ID]*ResponseRPC { - - resMap := make(map[ID]*ResponseRPC, 0) - for _, r := range res { - resMap[r.ID] = r - } - return resMap -} - -func (res ResponsesRPC) GetByID(id ID) *ResponseRPC { - - for _, r := range res { - if r.ID == id { - return r - } - } - return nil -} - -func (res ResponsesRPC) HasError() bool { - - for _, resp := range res { - if resp.Error != nil { - return true - } - } - return false -} - -func (responseRPC *ResponseRPC) GetObject(object interface{}) error { - - js, err := json.Marshal(responseRPC.Result) - if err != nil { - return err - } - err = json.Unmarshal(js, object) - if err != nil { - return err - } - return nil -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/string.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/string.go deleted file mode 100644 index 330d2fb..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc/string.go +++ /dev/null @@ -1,16 +0,0 @@ -package jsonrpc - -import ( - "fmt" -) - -func toString(v interface{}) string { - - switch s := v.(type) { - case string: - return s - case fmt.Stringer: - return s.String() - } - return "" -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/options.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/options.go deleted file mode 100644 index 7ff5291..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/options.go +++ /dev/null @@ -1,76 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc" - "time" -) - -type Option func(cli *ClientJsonRPC) - -func (cli *ClientJsonRPC) applyOpts(opts []Option) { - for _, op := range opts { - op(cli) - } -} - -func DecodeError(decoder ErrorDecoder) Option { - return func(cli *ClientJsonRPC) { - cli.errorDecoder = decoder - } -} - -func Cache(cache cache) Option { - return func(cli *ClientJsonRPC) { - cli.cache = cache - } -} - -func CircuitBreaker(cfg cb.Settings) Option { - return func(cli *ClientJsonRPC) { - cli.cbCfg = cfg - } -} - -func FallbackTTL(ttl time.Duration) Option { - return func(cli *ClientJsonRPC) { - cli.fallbackTTL = ttl - } -} - -func FallbackGameConnectionsErr(fallback fallbackGameConnections) Option { - return func(cli *ClientJsonRPC) { - cli.fallbackGameConnections = fallback - } -} - -func FallbackServerSettingsErr(fallback fallbackServerSettings) Option { - return func(cli *ClientJsonRPC) { - cli.fallbackServerSettings = fallback - } -} - -func Headers(headers ...interface{}) Option { - return func(cli *ClientJsonRPC) { - cli.rpcOpts = append(cli.rpcOpts, jsonrpc.HeaderFromCtx(headers...)) - } -} - -func Insecure() Option { - return func(cli *ClientJsonRPC) { - cli.rpcOpts = append(cli.rpcOpts, jsonrpc.Insecure()) - } -} - -func LogRequest() Option { - return func(cli *ClientJsonRPC) { - cli.rpcOpts = append(cli.rpcOpts, jsonrpc.LogRequest()) - } -} - -func LogOnError() Option { - return func(cli *ClientJsonRPC) { - cli.rpcOpts = append(cli.rpcOpts, jsonrpc.LogOnError()) - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-exchange.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-exchange.go deleted file mode 100644 index 522009d..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-exchange.go +++ /dev/null @@ -1,57 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" -) - -type requestServerSettingsGetConnectionsNum struct { - Token string `json:"token"` -} - -type responseServerSettingsGetConnectionsNum struct { - CountConn int `json:"countConn"` - Exists bool `json:"exists"` -} - -type requestServerSettingsHealthCheck struct { - Token string `json:"token"` -} - -type responseServerSettingsHealthCheck struct { - Exists bool `json:"exists"` -} - -type requestServerSettingsGetServerSettings struct { - Token string `json:"token"` -} - -type responseServerSettingsGetServerSettings struct { - Settings types.Settings `json:"settings"` -} - -type requestServerSettingsCreateRoom struct { - Token string `json:"token"` - CreateRoom types.CreateRoomRequest `json:"createRoom"` -} - -// Formal exchange type, please do not delete. -type responseServerSettingsCreateRoom struct{} - -type requestServerSettingsSetNotifyServer struct { - Token string `json:"token"` - Id uuid.UUID `json:"id"` - Url string `json:"url"` -} - -// Formal exchange type, please do not delete. -type responseServerSettingsSetNotifyServer struct{} - -type requestServerSettingsGetGameResults struct { - Token string `json:"token"` -} - -type responseServerSettingsGetGameResults struct { - GameConfigResults []types.GameConfigResults `json:"gameConfigResults"` -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-fallback.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-fallback.go deleted file mode 100644 index 7f9cdc6..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-fallback.go +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -type fallbackServerSettings interface { - GetConnectionsNum(err error) bool - HealthCheck(err error) bool - GetServerSettings(err error) bool - CreateRoom(err error) bool - SetNotifyServer(err error) bool - GetGameResults(err error) bool -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-jsonrpc.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-jsonrpc.go deleted file mode 100644 index fded17b..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/serversettings-jsonrpc.go +++ /dev/null @@ -1,354 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "context" - "fmt" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher" - "github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" -) - -type ClientServerSettings struct { - *ClientJsonRPC -} - -type retServerSettingsGetConnectionsNum = func(countConn int, exists bool, err error) -type retServerSettingsHealthCheck = func(exists bool, err error) -type retServerSettingsGetServerSettings = func(settings types.Settings, err error) -type retServerSettingsCreateRoom = func(err error) -type retServerSettingsSetNotifyServer = func(err error) -type retServerSettingsGetGameResults = func(gameConfigResults []types.GameConfigResults, err error) - -func (cli *ClientServerSettings) GetConnectionsNum(ctx context.Context, token string) (countConn int, exists bool, err error) { - - request := requestServerSettingsGetConnectionsNum{Token: token} - var response responseServerSettingsGetConnectionsNum - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "serversettings.getconnectionsnum", request) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetConnectionsNum - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return response.CountConn, response.Exists, err -} - -func (cli *ClientServerSettings) ReqGetConnectionsNum(ctx context.Context, callback retServerSettingsGetConnectionsNum, token string) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "serversettings.getconnectionsnum", - Params: requestServerSettingsGetConnectionsNum{Token: token}, - }} - if callback != nil { - var response responseServerSettingsGetConnectionsNum - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetConnectionsNum - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(response.CountConn, response.Exists, cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - -func (cli *ClientServerSettings) HealthCheck(ctx context.Context, token string) (exists bool, err error) { - - request := requestServerSettingsHealthCheck{Token: token} - var response responseServerSettingsHealthCheck - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "serversettings.healthcheck", request) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.HealthCheck - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return response.Exists, err -} - -func (cli *ClientServerSettings) ReqHealthCheck(ctx context.Context, callback retServerSettingsHealthCheck, token string) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "serversettings.healthcheck", - Params: requestServerSettingsHealthCheck{Token: token}, - }} - if callback != nil { - var response responseServerSettingsHealthCheck - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.HealthCheck - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(response.Exists, cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - -func (cli *ClientServerSettings) GetServerSettings(ctx context.Context, token string) (settings types.Settings, err error) { - - request := requestServerSettingsGetServerSettings{Token: token} - var response responseServerSettingsGetServerSettings - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "serversettings.getserversettings", request) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetServerSettings - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return response.Settings, err -} - -func (cli *ClientServerSettings) ReqGetServerSettings(ctx context.Context, callback retServerSettingsGetServerSettings, token string) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "serversettings.getserversettings", - Params: requestServerSettingsGetServerSettings{Token: token}, - }} - if callback != nil { - var response responseServerSettingsGetServerSettings - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetServerSettings - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(response.Settings, cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - -func (cli *ClientServerSettings) CreateRoom(ctx context.Context, token string, createRoom types.CreateRoomRequest) (err error) { - - request := requestServerSettingsCreateRoom{ - CreateRoom: createRoom, - Token: token, - } - var response responseServerSettingsCreateRoom - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "serversettings.createroom", request) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.CreateRoom - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return err -} - -func (cli *ClientServerSettings) ReqCreateRoom(ctx context.Context, callback retServerSettingsCreateRoom, token string, createRoom types.CreateRoomRequest) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "serversettings.createroom", - Params: requestServerSettingsCreateRoom{ - CreateRoom: createRoom, - Token: token, - }, - }} - if callback != nil { - var response responseServerSettingsCreateRoom - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.CreateRoom - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - -func (cli *ClientServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - - request := requestServerSettingsSetNotifyServer{ - Id: id, - Token: token, - Url: url, - } - var response responseServerSettingsSetNotifyServer - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "serversettings.setnotifyserver", request) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.SetNotifyServer - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return err -} - -func (cli *ClientServerSettings) ReqSetNotifyServer(ctx context.Context, callback retServerSettingsSetNotifyServer, token string, id uuid.UUID, url string) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "serversettings.setnotifyserver", - Params: requestServerSettingsSetNotifyServer{ - Id: id, - Token: token, - Url: url, - }, - }} - if callback != nil { - var response responseServerSettingsSetNotifyServer - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.SetNotifyServer - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - -func (cli *ClientServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - - request := requestServerSettingsGetGameResults{Token: token} - var response responseServerSettingsGetGameResults - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "serversettings.getgameresults", request) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetGameResults - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return response.GameConfigResults, err -} - -func (cli *ClientServerSettings) ReqGetGameResults(ctx context.Context, callback retServerSettingsGetGameResults, token string) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "serversettings.getgameresults", - Params: requestServerSettingsGetGameResults{Token: token}, - }} - if callback != nil { - var response responseServerSettingsGetGameResults - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetGameResults - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(response.GameConfigResults, cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/tracer.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/tracer.go deleted file mode 100644 index 14ef96b..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/tracer.go +++ /dev/null @@ -1,36 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -import ( - "context" - "github.com/gofiber/fiber/v2" - otg "github.com/opentracing/opentracing-go" - "github.com/rs/zerolog/log" - "net/http" - "strings" -) - -func extractSpan(ctx context.Context, opName string) (span otg.Span) { - - var opts []otg.StartSpanOption - span = otg.SpanFromContext(ctx) - - if span == nil { - log.Ctx(ctx).Debug().Msg("context does not contain span") - } else { - opts = append(opts, otg.ChildOf(span.Context())) - } - - span = otg.GlobalTracer().StartSpan(opName, opts...) - return -} - -func injectSpan(span otg.Span, request *fiber.Request) { - headers := make(http.Header) - if err := otg.GlobalTracer().Inject(span.Context(), otg.HTTPHeaders, otg.HTTPHeadersCarrier(headers)); err != nil { - log.Warn().Err(err).Msg("inject span to HTTP headers") - } - for key, values := range headers { - request.Header.Set(key, strings.Join(values, ";")) - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/version.go b/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/version.go deleted file mode 100644 index 2f4de49..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/version.go +++ /dev/null @@ -1,4 +0,0 @@ -// GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. -package tcpGameServer - -const VersionTg = "v2.3.49" diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/errors/errors.go b/vendor/github.com/ascenmmo/tcp-server/pkg/errors/errors.go index 339254b..b5106c1 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/errors/errors.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/errors/errors.go @@ -7,7 +7,7 @@ var ( ErrRoomNotFound = errors.New("room not found") ErrRoomIsExists = errors.New("room is exists") ErrRoomBadValue = errors.New("room bad value") - ErrTooManyRequests = errors.New("too many connection") + ErrTooManyRequests = errors.New("too many requests") ErrNotifyServerNotFound = errors.New("err notify server not found") ErrNotifyServerNotValid = errors.New("err notify server not valid") ErrGameConfigMarshalUserData = errors.New("err game config marshal user data") diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/game_config.go b/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/game_config.go deleted file mode 100644 index beba11e..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/game_config.go +++ /dev/null @@ -1,45 +0,0 @@ -package types - -import ( - tokentype "github.com/ascenmmo/token-generator/token_type" - "github.com/google/uuid" -) - -type GameConfigExecutor interface { - Name() string - Execute(clientInfo tokentype.Info, config SortingConfig, newUserData interface{}, oldResult GameConfigResults) (newResult GameConfigResults, err error) -} - -type GameConfigResults struct { - GameID uuid.UUID `json:"game_id"` - RoomID uuid.UUID `json:"room_id"` - Result map[string]interface{} `json:"result"` -} - -type GameConfigs struct { - GameID uuid.UUID `json:"game_id" bson:"_id"` - SortingConfig []SortingConfig `json:"sorting_config" bson:"sorting_config"` - IsExists bool `json:"-"` -} - -type SortingConfig struct { - Name string `json:"name" bson:"name"` - Params []ParamMetadata `json:"params" bson:"params"` - UseOnServerType string `json:"use_on_server_type" bson:"use_on_server_type"` - ResultName string `json:"result_name" bson:"result_name"` - ResultType string `json:"result_type" bson:"result_type"` - Executor GameConfigExecutor `json:"-"` -} - -type ParamMetadata struct { - ColumnName string `json:"column_name" bson:"column_name"` - ValueType string `json:"value_type" bson:"value_type"` -} - -func (g *GameConfigs) RemoveSortingConfig(name string) { - for i, v := range g.SortingConfig { - if v.Name == name { - g.SortingConfig = append(g.SortingConfig[:i], g.SortingConfig[i+1:]...) - } - } -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/network.go b/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/network.go deleted file mode 100644 index 89cb5ac..0000000 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/restconnection/types/network.go +++ /dev/null @@ -1,18 +0,0 @@ -package types - -import "github.com/google/uuid" - -type RequestSetMessage struct { - Server *uuid.UUID `json:"server,omitempty"` - Token string `json:"token,omitempty"` - Data any `json:"data"` -} - -type ResponseGetMessage struct { - DataArray []any `json:"dataArray"` -} - -type CreateRoomRequest struct { - TTL string `json:"time_to_live"` - GameConfigs GameConfigs `json:"gameConfigs"` -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/start/start.go b/vendor/github.com/ascenmmo/tcp-server/pkg/start/start.go index 676def2..4ed16e1 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/start/start.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/start/start.go @@ -5,18 +5,17 @@ import ( "fmt" "github.com/ascenmmo/tcp-server/internal/handler" "github.com/ascenmmo/tcp-server/internal/service" - configsService "github.com/ascenmmo/tcp-server/internal/service/configs_service" "github.com/ascenmmo/tcp-server/internal/storage" "github.com/ascenmmo/tcp-server/internal/utils" "github.com/ascenmmo/tcp-server/pkg/transport" tokengenerator "github.com/ascenmmo/token-generator/token_generator" "github.com/rs/zerolog" + "runtime" "time" ) -func StartTCP(ctx context.Context, address string, port string, token string, rateLimit int, dataTTL, gameConfigResultsTTl time.Duration, logger zerolog.Logger) (err error) { +func StartTCP(ctx context.Context, address string, port string, token string, rateLimit int, dataTTL time.Duration, logger zerolog.Logger, logWithMemoryUsage bool) (err error) { ramDB := memoryDB.NewMemoryDb(ctx, dataTTL) - gameConfigResultsDB := memoryDB.NewMemoryDb(ctx, gameConfigResultsTTl) rateLimitDB := memoryDB.NewMemoryDb(ctx, 1) tokenGenerator, err := tokengenerator.NewTokenGenerator(token) @@ -24,14 +23,17 @@ func StartTCP(ctx context.Context, address string, port string, token string, ra return err } - gameConfigsService := configsService.NewGameConfigsService(gameConfigResultsDB, tokenGenerator) - newService := service.NewService(tokenGenerator, ramDB, gameConfigsService, logger) + newService := service.NewService(tokenGenerator, ramDB, logger) rateLimitSerice := utils.NewRateLimit(rateLimit, rateLimitDB) connection := handler.NewRestConnection(rateLimitSerice, newService) serverSettings := handler.NewServerSettings(rateLimitSerice, newService) + if logWithMemoryUsage { + logMemoryUsage(logger) + } + services := []transport.Option{ transport.MaxBodySize(10 * 1024 * 1024), transport.GameConnections(transport.NewGameConnections(connection)), @@ -44,3 +46,19 @@ func StartTCP(ctx context.Context, address string, port string, token string, ra return srv.Fiber().Listen(":" + port) } + +func logMemoryUsage(logger zerolog.Logger) { + ticker := time.NewTicker(time.Second * 10) + go func() { + for range ticker.C { + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + logger.Info(). + Interface("num cpu", runtime.NumCPU()). + Interface("Memory Usage", stats.Alloc/1024/1024). + Interface("TotalAlloc", stats.TotalAlloc/1024/1024). + Interface("Sys", stats.Sys/1024/1024). + Interface("NumGC", stats.NumGC) + } + }() +} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-exchange.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-exchange.go index af6de38..f0e1910 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-exchange.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-exchange.go @@ -2,7 +2,7 @@ package transport import ( - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/google/uuid" ) diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-http.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-http.go index 7c19abd..09e2eef 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-http.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-http.go @@ -2,7 +2,7 @@ package transport import ( - "github.com/ascenmmo/tcp-server/pkg/restconnection" + "github.com/ascenmmo/tcp-server/pkg/api" "github.com/gofiber/fiber/v2" ) @@ -11,10 +11,10 @@ type httpGameConnections struct { maxBatchSize int maxParallelBatch int svc *serverGameConnections - base restconnection.GameConnections + base api.GameConnections } -func NewGameConnections(svcGameConnections restconnection.GameConnections) (srv *httpGameConnections) { +func NewGameConnections(svcGameConnections api.GameConnections) (srv *httpGameConnections) { srv = &httpGameConnections{ base: svcGameConnections, diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-logger.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-logger.go index 36d977c..2acafb9 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-logger.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-logger.go @@ -3,8 +3,8 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/ascenmmo/tcp-server/pkg/transport/viewer" "github.com/google/uuid" "github.com/rs/zerolog" @@ -13,11 +13,11 @@ import ( ) type loggerGameConnections struct { - next restconnection.GameConnections + next api.GameConnections } func loggerMiddlewareGameConnections() MiddlewareGameConnections { - return func(next restconnection.GameConnections) restconnection.GameConnections { + return func(next api.GameConnections) api.GameConnections { return &loggerGameConnections{next: next} } } diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-middleware.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-middleware.go index 5ca165f..5b1624d 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-middleware.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-middleware.go @@ -3,8 +3,8 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/google/uuid" ) @@ -12,7 +12,7 @@ type GameConnectionsSetSendMessage func(ctx context.Context, token string, messa type GameConnectionsGetMessage func(ctx context.Context, token string) (messages types.ResponseGetMessage, err error) type GameConnectionsRemoveUser func(ctx context.Context, token string, userID uuid.UUID) (err error) -type MiddlewareGameConnections func(next restconnection.GameConnections) restconnection.GameConnections +type MiddlewareGameConnections func(next api.GameConnections) api.GameConnections type MiddlewareGameConnectionsSetSendMessage func(next GameConnectionsSetSendMessage) GameConnectionsSetSendMessage type MiddlewareGameConnectionsGetMessage func(next GameConnectionsGetMessage) GameConnectionsGetMessage diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-server.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-server.go index 4a79da3..c156bd3 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-server.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-server.go @@ -3,13 +3,13 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/google/uuid" ) type serverGameConnections struct { - svc restconnection.GameConnections + svc api.GameConnections setSendMessage GameConnectionsSetSendMessage getMessage GameConnectionsGetMessage removeUser GameConnectionsRemoveUser @@ -25,7 +25,7 @@ type MiddlewareSetGameConnections interface { WithLog() } -func newServerGameConnections(svc restconnection.GameConnections) *serverGameConnections { +func newServerGameConnections(svc api.GameConnections) *serverGameConnections { return &serverGameConnections{ getMessage: svc.GetMessage, removeUser: svc.RemoveUser, diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-trace.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-trace.go index 980e028..b4d3cad 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-trace.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/gameconnections-trace.go @@ -3,17 +3,17 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/google/uuid" "github.com/opentracing/opentracing-go" ) type traceGameConnections struct { - next restconnection.GameConnections + next api.GameConnections } -func traceMiddlewareGameConnections(next restconnection.GameConnections) restconnection.GameConnections { +func traceMiddlewareGameConnections(next api.GameConnections) api.GameConnections { return &traceGameConnections{next: next} } diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/jsonrpc.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/jsonrpc.go index afa7342..ed566a0 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/jsonrpc.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/jsonrpc.go @@ -156,10 +156,6 @@ func (srv *Server) doSingleBatch(ctx *fiber.Ctx, request baseJsonRPC) (response return srv.httpServerSettings.getServerSettings(ctx, request) case "serversettings.createroom": return srv.httpServerSettings.createRoom(ctx, request) - case "serversettings.setnotifyserver": - return srv.httpServerSettings.setNotifyServer(ctx, request) - case "serversettings.getgameresults": - return srv.httpServerSettings.getGameResults(ctx, request) default: ext.Error.Set(span, true) span.SetTag("msg", "invalid method '"+methodNameOrigin+"'") diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-exchange.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-exchange.go index 403d229..ff29a66 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-exchange.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-exchange.go @@ -1,10 +1,7 @@ // GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. package transport -import ( - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" -) +import "github.com/ascenmmo/tcp-server/pkg/api/types" type requestServerSettingsGetConnectionsNum struct { Token string `json:"token"` @@ -38,20 +35,3 @@ type requestServerSettingsCreateRoom struct { // Formal exchange type, please do not delete. type responseServerSettingsCreateRoom struct{} - -type requestServerSettingsSetNotifyServer struct { - Token string `json:"token"` - Id uuid.UUID `json:"id"` - Url string `json:"url"` -} - -// Formal exchange type, please do not delete. -type responseServerSettingsSetNotifyServer struct{} - -type requestServerSettingsGetGameResults struct { - Token string `json:"token"` -} - -type responseServerSettingsGetGameResults struct { - GameConfigResults []types.GameConfigResults `json:"gameConfigResults"` -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-http.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-http.go index 7205ac9..af50975 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-http.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-http.go @@ -2,7 +2,7 @@ package transport import ( - "github.com/ascenmmo/tcp-server/pkg/restconnection" + "github.com/ascenmmo/tcp-server/pkg/api" "github.com/gofiber/fiber/v2" ) @@ -11,10 +11,10 @@ type httpServerSettings struct { maxBatchSize int maxParallelBatch int svc *serverServerSettings - base restconnection.ServerSettings + base api.ServerSettings } -func NewServerSettings(svcServerSettings restconnection.ServerSettings) (srv *httpServerSettings) { +func NewServerSettings(svcServerSettings api.ServerSettings) (srv *httpServerSettings) { srv = &httpServerSettings{ base: svcServerSettings, @@ -48,6 +48,4 @@ func (http *httpServerSettings) SetRoutes(route *fiber.App) { route.Post("/api/v1/rest/serverSettings/healthCheck", http.serveHealthCheck) route.Post("/api/v1/rest/serverSettings/getServerSettings", http.serveGetServerSettings) route.Post("/api/v1/rest/serverSettings/createRoom", http.serveCreateRoom) - route.Post("/api/v1/rest/serverSettings/setNotifyServer", http.serveSetNotifyServer) - route.Post("/api/v1/rest/serverSettings/getGameResults", http.serveGetGameResults) } diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-jsonrpc.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-jsonrpc.go index 538770e..22af17e 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-jsonrpc.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-jsonrpc.go @@ -238,120 +238,6 @@ func (http *httpServerSettings) createRoom(ctx *fiber.Ctx, requestBase baseJsonR } return } -func (http *httpServerSettings) serveSetNotifyServer(ctx *fiber.Ctx) (err error) { - return http.serveMethod(ctx, "setnotifyserver", http.setNotifyServer) -} -func (http *httpServerSettings) setNotifyServer(ctx *fiber.Ctx, requestBase baseJsonRPC) (responseBase *baseJsonRPC) { - - var err error - var request requestServerSettingsSetNotifyServer - - methodCtx := ctx.UserContext() - span := otg.SpanFromContext(methodCtx) - span.SetTag("method", "setNotifyServer") - - if requestBase.Params != nil { - if err = json.Unmarshal(requestBase.Params, &request); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "request body could not be decoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "request body could not be decoded: "+err.Error(), nil) - } - } - if requestBase.Version != Version { - ext.Error.Set(span, true) - span.SetTag("msg", "incorrect protocol version: "+requestBase.Version) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "incorrect protocol version: "+requestBase.Version, nil) - } - - if _token := string(ctx.Request().Header.Peek("Token")); _token != "" { - var token string - token = _token - request.Token = token - } - - var response responseServerSettingsSetNotifyServer - err = http.svc.SetNotifyServer(methodCtx, request.Token, request.Id, request.Url) - if err != nil { - if http.errorHandler != nil { - err = http.errorHandler(err) - } - ext.Error.Set(span, true) - span.SetTag("msg", err) - span.SetTag("errData", toString(err)) - code := internalError - if errCoder, ok := err.(withErrorCode); ok { - code = errCoder.Code() - } - return makeErrorResponseJsonRPC(requestBase.ID, code, err.Error(), err) - } - responseBase = &baseJsonRPC{ - ID: requestBase.ID, - Version: Version, - } - if responseBase.Result, err = json.Marshal(response); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "response body could not be encoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "response body could not be encoded: "+err.Error(), nil) - } - return -} -func (http *httpServerSettings) serveGetGameResults(ctx *fiber.Ctx) (err error) { - return http.serveMethod(ctx, "getgameresults", http.getGameResults) -} -func (http *httpServerSettings) getGameResults(ctx *fiber.Ctx, requestBase baseJsonRPC) (responseBase *baseJsonRPC) { - - var err error - var request requestServerSettingsGetGameResults - - methodCtx := ctx.UserContext() - span := otg.SpanFromContext(methodCtx) - span.SetTag("method", "getGameResults") - - if requestBase.Params != nil { - if err = json.Unmarshal(requestBase.Params, &request); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "request body could not be decoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "request body could not be decoded: "+err.Error(), nil) - } - } - if requestBase.Version != Version { - ext.Error.Set(span, true) - span.SetTag("msg", "incorrect protocol version: "+requestBase.Version) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "incorrect protocol version: "+requestBase.Version, nil) - } - - if _token := string(ctx.Request().Header.Peek("Token")); _token != "" { - var token string - token = _token - request.Token = token - } - - var response responseServerSettingsGetGameResults - response.GameConfigResults, err = http.svc.GetGameResults(methodCtx, request.Token) - if err != nil { - if http.errorHandler != nil { - err = http.errorHandler(err) - } - ext.Error.Set(span, true) - span.SetTag("msg", err) - span.SetTag("errData", toString(err)) - code := internalError - if errCoder, ok := err.(withErrorCode); ok { - code = errCoder.Code() - } - return makeErrorResponseJsonRPC(requestBase.ID, code, err.Error(), err) - } - responseBase = &baseJsonRPC{ - ID: requestBase.ID, - Version: Version, - } - if responseBase.Result, err = json.Marshal(response); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "response body could not be encoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "response body could not be encoded: "+err.Error(), nil) - } - return -} func (http *httpServerSettings) serveMethod(ctx *fiber.Ctx, methodName string, methodHandler methodJsonRPC) (err error) { span := otg.SpanFromContext(ctx.UserContext()) @@ -475,10 +361,6 @@ func (http *httpServerSettings) doSingleBatch(ctx *fiber.Ctx, request baseJsonRP return http.getServerSettings(ctx, request) case "createroom": return http.createRoom(ctx, request) - case "setnotifyserver": - return http.setNotifyServer(ctx, request) - case "getgameresults": - return http.getGameResults(ctx, request) default: ext.Error.Set(span, true) span.SetTag("msg", "invalid method '"+methodNameOrigin+"'") diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-logger.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-logger.go index 2bc70f5..e11f609 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-logger.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-logger.go @@ -3,21 +3,20 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/ascenmmo/tcp-server/pkg/transport/viewer" - "github.com/google/uuid" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "time" ) type loggerServerSettings struct { - next restconnection.ServerSettings + next api.ServerSettings } func loggerMiddlewareServerSettings() MiddlewareServerSettings { - return func(next restconnection.ServerSettings) restconnection.ServerSettings { + return func(next api.ServerSettings) api.ServerSettings { return &loggerServerSettings{next: next} } } @@ -103,45 +102,3 @@ func (m loggerServerSettings) CreateRoom(ctx context.Context, token string, crea }(time.Now()) return m.next.CreateRoom(ctx, token, createRoom) } - -func (m loggerServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - logger := log.Ctx(ctx).With().Str("service", "ServerSettings").Str("method", "setNotifyServer").Logger() - defer func(begin time.Time) { - logHandle := func(ev *zerolog.Event) { - fields := map[string]interface{}{ - "request": viewer.Sprintf("%+v", requestServerSettingsSetNotifyServer{ - Id: id, - Token: token, - Url: url, - }), - "response": viewer.Sprintf("%+v", responseServerSettingsSetNotifyServer{}), - } - ev.Fields(fields).Str("took", time.Since(begin).String()) - } - if err != nil { - logger.Error().Err(err).Func(logHandle).Msg("call setNotifyServer") - return - } - logger.Info().Func(logHandle).Msg("call setNotifyServer") - }(time.Now()) - return m.next.SetNotifyServer(ctx, token, id, url) -} - -func (m loggerServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - logger := log.Ctx(ctx).With().Str("service", "ServerSettings").Str("method", "getGameResults").Logger() - defer func(begin time.Time) { - logHandle := func(ev *zerolog.Event) { - fields := map[string]interface{}{ - "request": viewer.Sprintf("%+v", requestServerSettingsGetGameResults{Token: token}), - "response": viewer.Sprintf("%+v", responseServerSettingsGetGameResults{GameConfigResults: gameConfigResults}), - } - ev.Fields(fields).Str("took", time.Since(begin).String()) - } - if err != nil { - logger.Error().Err(err).Func(logHandle).Msg("call getGameResults") - return - } - logger.Info().Func(logHandle).Msg("call getGameResults") - }(time.Now()) - return m.next.GetGameResults(ctx, token) -} diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-middleware.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-middleware.go index 85eed70..3c160d5 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-middleware.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-middleware.go @@ -3,23 +3,18 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" ) type ServerSettingsGetConnectionsNum func(ctx context.Context, token string) (countConn int, exists bool, err error) type ServerSettingsHealthCheck func(ctx context.Context, token string) (exists bool, err error) type ServerSettingsGetServerSettings func(ctx context.Context, token string) (settings types.Settings, err error) type ServerSettingsCreateRoom func(ctx context.Context, token string, createRoom types.CreateRoomRequest) (err error) -type ServerSettingsSetNotifyServer func(ctx context.Context, token string, id uuid.UUID, url string) (err error) -type ServerSettingsGetGameResults func(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) -type MiddlewareServerSettings func(next restconnection.ServerSettings) restconnection.ServerSettings +type MiddlewareServerSettings func(next api.ServerSettings) api.ServerSettings type MiddlewareServerSettingsGetConnectionsNum func(next ServerSettingsGetConnectionsNum) ServerSettingsGetConnectionsNum type MiddlewareServerSettingsHealthCheck func(next ServerSettingsHealthCheck) ServerSettingsHealthCheck type MiddlewareServerSettingsGetServerSettings func(next ServerSettingsGetServerSettings) ServerSettingsGetServerSettings type MiddlewareServerSettingsCreateRoom func(next ServerSettingsCreateRoom) ServerSettingsCreateRoom -type MiddlewareServerSettingsSetNotifyServer func(next ServerSettingsSetNotifyServer) ServerSettingsSetNotifyServer -type MiddlewareServerSettingsGetGameResults func(next ServerSettingsGetGameResults) ServerSettingsGetGameResults diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-server.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-server.go index 36604d9..c6911e4 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-server.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-server.go @@ -3,19 +3,16 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" ) type serverServerSettings struct { - svc restconnection.ServerSettings + svc api.ServerSettings getConnectionsNum ServerSettingsGetConnectionsNum healthCheck ServerSettingsHealthCheck getServerSettings ServerSettingsGetServerSettings createRoom ServerSettingsCreateRoom - setNotifyServer ServerSettingsSetNotifyServer - getGameResults ServerSettingsGetGameResults } type MiddlewareSetServerSettings interface { @@ -24,21 +21,17 @@ type MiddlewareSetServerSettings interface { WrapHealthCheck(m MiddlewareServerSettingsHealthCheck) WrapGetServerSettings(m MiddlewareServerSettingsGetServerSettings) WrapCreateRoom(m MiddlewareServerSettingsCreateRoom) - WrapSetNotifyServer(m MiddlewareServerSettingsSetNotifyServer) - WrapGetGameResults(m MiddlewareServerSettingsGetGameResults) WithTrace() WithLog() } -func newServerServerSettings(svc restconnection.ServerSettings) *serverServerSettings { +func newServerServerSettings(svc api.ServerSettings) *serverServerSettings { return &serverServerSettings{ createRoom: svc.CreateRoom, getConnectionsNum: svc.GetConnectionsNum, - getGameResults: svc.GetGameResults, getServerSettings: svc.GetServerSettings, healthCheck: svc.HealthCheck, - setNotifyServer: svc.SetNotifyServer, svc: svc, } } @@ -49,8 +42,6 @@ func (srv *serverServerSettings) Wrap(m MiddlewareServerSettings) { srv.healthCheck = srv.svc.HealthCheck srv.getServerSettings = srv.svc.GetServerSettings srv.createRoom = srv.svc.CreateRoom - srv.setNotifyServer = srv.svc.SetNotifyServer - srv.getGameResults = srv.svc.GetGameResults } func (srv *serverServerSettings) GetConnectionsNum(ctx context.Context, token string) (countConn int, exists bool, err error) { @@ -69,14 +60,6 @@ func (srv *serverServerSettings) CreateRoom(ctx context.Context, token string, c return srv.createRoom(ctx, token, createRoom) } -func (srv *serverServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - return srv.setNotifyServer(ctx, token, id, url) -} - -func (srv *serverServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - return srv.getGameResults(ctx, token) -} - func (srv *serverServerSettings) WrapGetConnectionsNum(m MiddlewareServerSettingsGetConnectionsNum) { srv.getConnectionsNum = m(srv.getConnectionsNum) } @@ -93,14 +76,6 @@ func (srv *serverServerSettings) WrapCreateRoom(m MiddlewareServerSettingsCreate srv.createRoom = m(srv.createRoom) } -func (srv *serverServerSettings) WrapSetNotifyServer(m MiddlewareServerSettingsSetNotifyServer) { - srv.setNotifyServer = m(srv.setNotifyServer) -} - -func (srv *serverServerSettings) WrapGetGameResults(m MiddlewareServerSettingsGetGameResults) { - srv.getGameResults = m(srv.getGameResults) -} - func (srv *serverServerSettings) WithTrace() { srv.Wrap(traceMiddlewareServerSettings) } diff --git a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-trace.go b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-trace.go index 1ed453d..61abef2 100644 --- a/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-trace.go +++ b/vendor/github.com/ascenmmo/tcp-server/pkg/transport/serversettings-trace.go @@ -3,17 +3,16 @@ package transport import ( "context" - "github.com/ascenmmo/tcp-server/pkg/restconnection" - "github.com/ascenmmo/tcp-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/tcp-server/pkg/api" + "github.com/ascenmmo/tcp-server/pkg/api/types" "github.com/opentracing/opentracing-go" ) type traceServerSettings struct { - next restconnection.ServerSettings + next api.ServerSettings } -func traceMiddlewareServerSettings(next restconnection.ServerSettings) restconnection.ServerSettings { +func traceMiddlewareServerSettings(next api.ServerSettings) api.ServerSettings { return &traceServerSettings{next: next} } @@ -40,15 +39,3 @@ func (svc traceServerSettings) CreateRoom(ctx context.Context, token string, cre span.SetTag("method", "CreateRoom") return svc.next.CreateRoom(ctx, token, createRoom) } - -func (svc traceServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - span := opentracing.SpanFromContext(ctx) - span.SetTag("method", "SetNotifyServer") - return svc.next.SetNotifyServer(ctx, token, id, url) -} - -func (svc traceServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - span := opentracing.SpanFromContext(ctx) - span.SetTag("method", "GetGameResults") - return svc.next.GetGameResults(ctx, token) -} diff --git a/vendor/github.com/ascenmmo/udp-server/env/env.go b/vendor/github.com/ascenmmo/udp-server/env/env.go deleted file mode 100644 index c573851..0000000 --- a/vendor/github.com/ascenmmo/udp-server/env/env.go +++ /dev/null @@ -1,9 +0,0 @@ -package env - -var ( - ServerAddress = "127.0.0.1" // Server address - TCPPort = "8081" // Port for TCP connections - UDPPort = "4500" // Port for UDP connections - TokenKey = "_remember_token_must_be_32_bytes" // Unique token for authentication - MaxRequestPerSecond = 200 // Maximum number of requests per second -) diff --git a/vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go b/vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go deleted file mode 100644 index 2edf21c..0000000 --- a/vendor/github.com/ascenmmo/udp-server/internal/connection/notify_servers.go +++ /dev/null @@ -1,98 +0,0 @@ -package connection - -import ( - "encoding/json" - "github.com/google/uuid" - "net" -) - -type NotifyServers interface { - NotifyServers(ids []uuid.UUID, request []byte) error - AddServer(ID uuid.UUID, addr string) error -} - -type notifier struct { - servers []*server -} - -func NewNotifierServers() NotifyServers { - return ¬ifier{} -} - -type server struct { - ID uuid.UUID `json:"id"` - Addr string `json:"addr"` - Connection *net.UDPConn - Add *net.UDPConn -} - -func (n *notifier) NotifyServers(ids []uuid.UUID, request []byte) error { - if len(n.servers) == 0 { - return nil - } - marshal, err := json.Marshal(request) - if err != nil { - return err - } - for _, id := range ids { - for i, server := range n.servers { - if server.ID == id { - _, err = n.servers[i].Connection.Write(marshal) - if err != nil { - err = n.servers[i].Connect() - if err != nil { - n.RemoveNotifyServer(id) - return err - } - _, err = n.servers[i].Connection.Write(marshal) - return err - } - } - } - } - return nil -} - -func (n *notifier) AddServer(ID uuid.UUID, addr string) error { - newServer := &server{ - ID: ID, - Addr: addr, - } - err := newServer.Connect() - if err != nil { - return err - } - for i, s := range n.servers { - if s.ID == ID { - n.servers[i] = newServer - return nil - } - } - n.servers = append(n.servers, newServer) - return nil -} - -func (n *notifier) RemoveNotifyServer(id uuid.UUID) { - for i, s := range n.servers { - if s.ID == id { - _ = n.servers[i].Connection.Close() - n.servers = append(n.servers[:i], n.servers[i+1:]...) - } - } -} - -func (s *server) Connect() error { - serverAddr, err := net.ResolveUDPAddr("udp", s.Addr) - if err != nil { - return err - } - - conn, err := net.DialUDP("udp", nil, serverAddr) - if err != nil { - return err - } - - s.Connection = conn - - return nil -} diff --git a/vendor/github.com/ascenmmo/udp-server/internal/handler/tcp/server_settings.go b/vendor/github.com/ascenmmo/udp-server/internal/handler/tcp/server_settings.go index 275bb74..ec5cb4b 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/handler/tcp/server_settings.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/handler/tcp/server_settings.go @@ -4,8 +4,8 @@ import ( "context" "github.com/ascenmmo/udp-server/internal/service" "github.com/ascenmmo/udp-server/internal/utils" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/ascenmmo/udp-server/pkg/errors" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" "github.com/google/uuid" ) @@ -45,25 +45,11 @@ func (r *ServerSettings) CreateRoom(ctx context.Context, token string, createRoo if limited { return errors.ErrTooManyRequests } - err = r.server.CreateRoom(token, createRoom.GameConfigs) - return -} - -func (r *ServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - limited := r.rateLimit.IsLimited(token) - if limited { - return gameConfigResults, errors.ErrTooManyRequests - } - gameConfigResults, err = r.server.GetGameResults(token) + err = r.server.CreateRoom(token) return } func (r *ServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - limited := r.rateLimit.IsLimited(token) - if limited { - return errors.ErrTooManyRequests - } - err = r.server.SetRoomNotifyServer(token, id, url) return } diff --git a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go b/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go deleted file mode 100644 index 34496f5..0000000 --- a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/count_results.go +++ /dev/null @@ -1,191 +0,0 @@ -package configsService - -import ( - tokentype "github.com/ascenmmo/token-generator/token_type" - "github.com/ascenmmo/udp-server/pkg/errors" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" -) - -func GetCountingFunctions() (functions []types.GameConfigExecutor) { - functions = []types.GameConfigExecutor{ - &IncrementResult{}, - &DecrementResult{}, - &AdditionDataResultToOld{}, - &SubtractDataResultToOld{}, - } - return functions -} - -type IncrementResult struct { -} - -func (c *IncrementResult) Name() string { - return "IncrementResult" -} - -func (c *IncrementResult) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - oldResult.Result[config.ResultName] = incrementValue(oldValue) - - return oldResult, nil -} - -type DecrementResult struct { -} - -func (c *DecrementResult) Name() string { - return "DecrementResult" -} - -func (c *DecrementResult) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - oldResult.Result[config.ResultName] = decrementValue(oldValue) - - return oldResult, nil -} - -type AdditionDataResultToOld struct{} - -func (c *AdditionDataResultToOld) Name() string { - return "AdditionDataResultToOld" -} - -func (c *AdditionDataResultToOld) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - for _, newValue := range values { - oldValue = additionValues(oldValue, newValue, config.ResultType) - } - - oldResult.Result[config.ResultName] = oldValue - - return oldResult, nil -} - -type SubtractDataResultToOld struct{} - -func (c *SubtractDataResultToOld) Name() string { - return "SubtractDataResultToOld" -} - -func (c *SubtractDataResultToOld) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - for _, newValue := range values { - oldValue = subtractValues(oldValue, newValue, config.ResultType) - } - - oldResult.Result[config.ResultName] = oldValue - - return oldResult, nil -} - -func parseUserData(newUserData interface{}) (map[string]interface{}, error) { - data, ok := newUserData.(map[string]interface{}) - if !ok { - return nil, errors.ErrGameConfigMarshalUserData - } - return data, nil -} diff --git a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go b/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go deleted file mode 100644 index 360df8b..0000000 --- a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/game_configs.go +++ /dev/null @@ -1,118 +0,0 @@ -package configsService - -import ( - tokengenerator "github.com/ascenmmo/token-generator/token_generator" - tokentype "github.com/ascenmmo/token-generator/token_type" - memoryDB "github.com/ascenmmo/udp-server/internal/storage" - "github.com/ascenmmo/udp-server/internal/utils" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" - "github.com/google/uuid" -) - -type GameConfigsService interface { - Do(token string, clientInfo tokentype.Info, gameConfig types.GameConfigs, data interface{}) - GetDeletedRoomsResults(clientInfo tokentype.Info, onlinePlayersTokens []string) (results []types.GameConfigResults, ok bool) - SetServerExecuteToGameConfig(clientInfo tokentype.Info, gameConfig types.GameConfigs) (newGameConfig types.GameConfigs) -} - -type gameConfig struct { - allGameConfigExecutor []types.GameConfigExecutor - token tokengenerator.TokenGenerator - storage memoryDB.IMemoryDB -} - -func (g *gameConfig) Do(token string, clientInfo tokentype.Info, gameConfig types.GameConfigs, data interface{}) { - if !gameConfig.IsExists { - return - } - results, _ := g.getOldResults(clientInfo) - for _, sorting := range gameConfig.SortingConfig { - if sorting.Executor == nil { - continue - } - newResult, err := sorting.Executor.Execute(clientInfo, sorting, data, results) - if err != nil { - continue - } - results = newResult - } - g.storage.AddConnection(token) - g.serOldResults(clientInfo, results) -} - -func (g *gameConfig) GetDeletedRoomsResults(_ tokentype.Info, onlinePlayersTokens []string) (results []types.GameConfigResults, ok bool) { - ids := g.storage.GetAllConnection() - - uniqueTokens := make(map[string]struct{}) - for _, token := range onlinePlayersTokens { - uniqueTokens[token] = struct{}{} - } - - var offlinePlayers []string - for _, token := range ids { - if _, exists := uniqueTokens[token]; !exists { - offlinePlayers = append(offlinePlayers, token) - } - } - - clientsInfo := make(map[string]tokentype.Info) - for _, token := range offlinePlayers { - info, _ := g.token.ParseToken(token) - if info.RoomID != uuid.Nil { - clientsInfo[info.RoomID.String()] = info - } - - } - - for _, info := range clientsInfo { - configResults, ok := g.getOldResults(info) - if !ok { - continue - } - results = append(results, configResults) - } - - return results, len(results) > 0 -} - -func (g *gameConfig) SetServerExecuteToGameConfig(_ tokentype.Info, gameConfig types.GameConfigs) (newGameConfig types.GameConfigs) { - isConfigExecutorFound := false - for i, conf := range gameConfig.SortingConfig { - for _, executor := range g.allGameConfigExecutor { - if executor.Name() == conf.Name { - gameConfig.SortingConfig[i].Executor = executor - isConfigExecutorFound = true - } - } - } - gameConfig.IsExists = isConfigExecutorFound - return gameConfig -} - -func (g *gameConfig) getOldResults(clientInfo tokentype.Info) (configResults types.GameConfigResults, ok bool) { - key := utils.GenerateRoomKey(clientInfo) - data, ok := g.storage.GetData(key) - if ok { - if configResults, ok = data.(types.GameConfigResults); ok { - return configResults, true - } - } - return types.GameConfigResults{ - GameID: clientInfo.GameID, - RoomID: clientInfo.RoomID, - Result: make(map[string]interface{}), - }, false -} - -func (g *gameConfig) serOldResults(clientInfo tokentype.Info, configResults types.GameConfigResults) { - key := utils.GenerateRoomKey(clientInfo) - g.storage.SetData(key, configResults) -} - -func getAllFunctions() []types.GameConfigExecutor { - return GetCountingFunctions() -} - -func NewGameConfigsService(storage memoryDB.IMemoryDB, token tokengenerator.TokenGenerator) GameConfigsService { - return &gameConfig{allGameConfigExecutor: getAllFunctions(), storage: storage, token: token} -} diff --git a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/numbers.go b/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/numbers.go deleted file mode 100644 index 34bf68a..0000000 --- a/vendor/github.com/ascenmmo/udp-server/internal/service/configs_service/numbers.go +++ /dev/null @@ -1,78 +0,0 @@ -package configsService - -func incrementValue(value any) any { - switch v := value.(type) { - case int: - return v + 1 - case int32: - return v + 1 - case int64: - return v + 1 - case float32: - return v + 1 - case float64: - return v + 1 - default: - return value - } -} - -func decrementValue(value any) any { - switch v := value.(type) { - case int: - return v - 1 - case int32: - return v - 1 - case int64: - return v - 1 - case float32: - return v - 1 - case float64: - return v - 1 - default: - return value - } -} - -func additionValues(v1, v2 any, expectedType string) any { - switch expectedType { - case "int": - return int(toFloat64(v1) + toFloat64(v2)) - case "int64": - return int64(toFloat64(v1) + toFloat64(v2)) - case "float64": - return toFloat64(v1) + toFloat64(v2) - default: - return nil // В случае неподдерживаемого типа - } -} - -func subtractValues(v1, v2 any, expectedType string) any { - switch expectedType { - case "int": - return int(toFloat64(v1) - toFloat64(v2)) - case "int64": - return int64(toFloat64(v1) + toFloat64(v2)) - case "float64": - return toFloat64(v1) - toFloat64(v2) - default: - return nil // В случае неподдерживаемого типа - } -} - -func toFloat64(value any) float64 { - switch v := value.(type) { - case int: - return float64(v) - case int32: - return float64(v) - case int64: - return float64(v) - case float32: - return float64(v) - case float64: - return v - default: - return 0 - } -} diff --git a/vendor/github.com/ascenmmo/udp-server/internal/service/service.go b/vendor/github.com/ascenmmo/udp-server/internal/service/service.go index 17c9af5..f4e4205 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/service/service.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/service/service.go @@ -5,12 +5,10 @@ import ( tokengenerator "github.com/ascenmmo/token-generator/token_generator" tokentype "github.com/ascenmmo/token-generator/token_type" "github.com/ascenmmo/udp-server/internal/connection" - "github.com/ascenmmo/udp-server/internal/entities" - configsService "github.com/ascenmmo/udp-server/internal/service/configs_service" memoryDB "github.com/ascenmmo/udp-server/internal/storage" "github.com/ascenmmo/udp-server/internal/utils" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/ascenmmo/udp-server/pkg/errors" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" "github.com/google/uuid" "github.com/rs/zerolog" "time" @@ -18,19 +16,15 @@ import ( type Service interface { GetConnectionsNum() (countConn int, exists bool) - CreateRoom(token string, configs types.GameConfigs) error - GetUsersAndMessage(ds connection.DataSender, req []byte) (users []entities.User, msg []byte, err error) - NotifyAllServers(clientInfo tokentype.Info, reqreq []byte) (err error) + CreateRoom(token string) error + GetUsersAndMessage(ds connection.DataSender, req []byte) (users []types.User, msg []byte, err error) RemoveUser(ds connection.DataSender, userID uuid.UUID) (err error) - SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) - GetGameResults(token string) (results []types.GameConfigResults, err error) } type service struct { maxConnections uint64 - storage memoryDB.IMemoryDB - gameConfigService configsService.GameConfigsService + storage memoryDB.IMemoryDB token tokengenerator.TokenGenerator logger zerolog.Logger @@ -45,7 +39,7 @@ func (s *service) GetConnectionsNum() (countConn int, exists bool) { return count, true } -func (s *service) CreateRoom(token string, configs types.GameConfigs) error { +func (s *service) CreateRoom(token string) error { clientInfo, err := s.token.ParseToken(token) if err != nil { return err @@ -58,87 +52,26 @@ func (s *service) CreateRoom(token string, configs types.GameConfigs) error { return errors.ErrRoomIsExists } - configs = s.gameConfigService.SetServerExecuteToGameConfig(clientInfo, configs) - - s.setRoom(clientInfo, &entities.Room{ - GameID: clientInfo.GameID, - RoomID: clientInfo.RoomID, - GameConfigs: configs, + s.setRoom(clientInfo, &types.Room{ + GameID: clientInfo.GameID, + RoomID: clientInfo.RoomID, }) return nil } -func (s *service) SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) { - //clientInfo, err := s.token.ParseToken(token) - //if err != nil { - // return err - //} - // - //room, err := s.getRoom(clientInfo) - //if err != nil { - // return err - //} - // - //room.SetServerID(id) - // - //data, _ := s.storage.GetData(utils.GenerateNotifyServerKey()) - // - //server, ok := data.(connection.NotifyServers) - //if !ok { - // s.logger.Warn().Msg("NotifyServers cant get interfase") - // server = connection.NewNotifierServers() - //} - // - //err = server.AddServer(id, url) - //if err != nil { - // return err - //} - // - //s.storage.SetData(utils.GenerateNotifyServerKey(), server) - - return nil -} - -func (s *service) NotifyAllServers(clientInfo tokentype.Info, request []byte) (err error) { - //room, err := s.getRoom(clientInfo) - //if err != nil { - // return err - //} - //if len(room.ServerID) == 0 { - // return nil - //} - // - //data, ok := s.storage.GetData(utils.GenerateNotifyServerKey()) - //if !ok { - // return errors.ErrNotifyServerNotFound - //} - // - //servers, ok := data.(connection.NotifyServers) - //if !ok { - // return errors.ErrNotifyServerNotValid - //} - // - //err = servers.NotifyServers(room.ServerID, request) - //if err != nil { - // return err - //} - - return nil -} - -func (s *service) GetUsersAndMessage(ds connection.DataSender, req []byte) (users []entities.User, msg []byte, err error) { +func (s *service) GetUsersAndMessage(ds connection.DataSender, req []byte) (users []types.User, msg []byte, err error) { clientInfo, room, err := s.getRoom(ds) if err != nil { clientInfo, err = s.setNewUser(ds, req) if err != nil { return nil, nil, err } - return append(users, entities.User{Connection: ds}), []byte(clientInfo.UserID.String()), nil + return append(users, types.User{Connection: ds}), []byte(clientInfo.UserID.String()), nil } if len(req) == 454 || len(req) == 343 { - return append(users, entities.User{Connection: ds}), []byte(clientInfo.UserID.String()), nil + return append(users, types.User{Connection: ds}), []byte(clientInfo.UserID.String()), nil } usersData := room.GetUser() @@ -166,21 +99,6 @@ func (s *service) RemoveUser(ds connection.DataSender, userID uuid.UUID) (err er return nil } -func (s *service) GetGameResults(token string) (results []types.GameConfigResults, err error) { - clientInfo, err := s.token.ParseToken(token) - if err != nil { - return results, err - } - - playersOnline := s.storage.GetAllConnection() - roomsResults, ok := s.gameConfigService.GetDeletedRoomsResults(clientInfo, playersOnline) - if !ok { - return results, errors.ErrGameResultsNotFound - } - - return roomsResults, nil -} - func (s *service) setNewUser(ds connection.DataSender, req []byte) (clientInfo *tokentype.Info, err error) { token := string(req) @@ -197,12 +115,12 @@ func (s *service) setNewUser(ds connection.DataSender, req []byte) (clientInfo * return clientInfo, errors.ErrRoomNotFound } - room, ok := roomData.(*entities.Room) + room, ok := roomData.(*types.Room) if !ok { return clientInfo, errors.ErrRoomBadValue } - room.SetUser(&entities.User{ + room.SetUser(&types.User{ ID: info.UserID, Connection: ds, }) @@ -212,7 +130,7 @@ func (s *service) setNewUser(ds connection.DataSender, req []byte) (clientInfo * return clientInfo, nil } -func (s *service) getRoom(ds connection.DataSender) (clientInfo *tokentype.Info, room *entities.Room, err error) { +func (s *service) getRoom(ds connection.DataSender) (clientInfo *tokentype.Info, room *types.Room, err error) { client, ok := s.storage.GetData(ds.GetID()) if !ok { return nil, nil, errors.ErrUserNotFound @@ -230,7 +148,7 @@ func (s *service) getRoom(ds connection.DataSender) (clientInfo *tokentype.Info, return nil, nil, errors.ErrRoomNotFound } - room, ok = roomData.(*entities.Room) + room, ok = roomData.(*types.Room) if !ok { return nil, nil, errors.ErrRoomBadValue } @@ -238,7 +156,7 @@ func (s *service) getRoom(ds connection.DataSender) (clientInfo *tokentype.Info, return &info, room, nil } -func (s *service) getRoomByClientInfo(clientInfo tokentype.Info) (room *entities.Room, err error) { +func (s *service) getRoomByClientInfo(clientInfo tokentype.Info) (room *types.Room, err error) { roomKey := utils.GenerateRoomKey(clientInfo) roomData, ok := s.storage.GetData(roomKey) @@ -246,7 +164,7 @@ func (s *service) getRoomByClientInfo(clientInfo tokentype.Info) (room *entities return nil, errors.ErrRoomNotFound } - room, ok = roomData.(*entities.Room) + room, ok = roomData.(*types.Room) if !ok { return nil, errors.ErrRoomBadValue } @@ -254,18 +172,17 @@ func (s *service) getRoomByClientInfo(clientInfo tokentype.Info) (room *entities return room, nil } -func (s *service) setRoom(clientInfo tokentype.Info, room *entities.Room) { +func (s *service) setRoom(clientInfo tokentype.Info, room *types.Room) { roomKey := utils.GenerateRoomKey(clientInfo) s.storage.SetData(roomKey, room) } -func NewService(token tokengenerator.TokenGenerator, storage memoryDB.IMemoryDB, gameConfigService configsService.GameConfigsService, logger zerolog.Logger) Service { +func NewService(token tokengenerator.TokenGenerator, storage memoryDB.IMemoryDB, logger zerolog.Logger) Service { srv := &service{ - maxConnections: uint64(types.CountConnectionsMAX()), - storage: storage, - token: token, - gameConfigService: gameConfigService, - logger: logger, + maxConnections: uint64(types.CountConnectionsMAX()), + storage: storage, + token: token, + logger: logger, } go func() { ticker := time.NewTicker(time.Second * 3) diff --git a/vendor/github.com/ascenmmo/udp-server/internal/storage/memory_db.go b/vendor/github.com/ascenmmo/udp-server/internal/storage/memory_db.go index ff323f9..0d52a2d 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/storage/memory_db.go +++ b/vendor/github.com/ascenmmo/udp-server/internal/storage/memory_db.go @@ -2,8 +2,6 @@ package memoryDB import ( "context" - "fmt" - "runtime" "sync" "time" ) @@ -40,16 +38,6 @@ type connections struct { count int } -func NewMemoryDb(ctx context.Context, dataTTL time.Duration) *MemoryDb { - db := &MemoryDb{ - userData: &userData{storage: sync.Map{}}, - connections: &connections{storage: sync.Map{}}, - dataTTL: dataTTL, - } - go db.Run(ctx) - return db -} - func (db *MemoryDb) GetData(key string) (any, bool) { value, ok := db.userData.storage.Load(key) if !ok { @@ -128,12 +116,14 @@ func (db *MemoryDb) removeOldData() { return true }) - db.logMemoryUsage() } -func (db *MemoryDb) logMemoryUsage() { - var stats runtime.MemStats - runtime.ReadMemStats(&stats) - fmt.Printf("Memory Usage: Alloc = %v MiB, TotalAlloc = %v MiB, Sys = %v MiB, NumGC = %v\n", - stats.Alloc/1024/1024, stats.TotalAlloc/1024/1024, stats.Sys/1024/1024, stats.NumGC) +func NewMemoryDb(ctx context.Context, dataTTL time.Duration) *MemoryDb { + db := &MemoryDb{ + userData: &userData{storage: sync.Map{}}, + connections: &connections{storage: sync.Map{}}, + dataTTL: dataTTL, + } + go db.Run(ctx) + return db } diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/server_settings.go b/vendor/github.com/ascenmmo/udp-server/pkg/api/server_settings.go similarity index 79% rename from vendor/github.com/ascenmmo/udp-server/pkg/restconnection/server_settings.go rename to vendor/github.com/ascenmmo/udp-server/pkg/api/server_settings.go index bd16dbb..d089a78 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/server_settings.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/api/server_settings.go @@ -1,19 +1,20 @@ // @tg version=1.0.3 // @tg backend="Asenmmo" // @tg title=`Ascenmmo Rest API` -// @tg servers=`http://stage.ascenmmo.com;stage cluster` +// @tg servers=`ascenmmo.com` // //go:generate tg transport --services . --out ../../pkg/transport --outSwagger ../../pkg/swagger.yaml //go:generate tg client -go --services . --outPath ../../pkg/clients/udpGameServer -package restconnection +package api import ( "context" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/google/uuid" ) +// ServerSettings // @tg http-prefix=api/v1/udp/ // @tg jsonRPC-server log trace // @tg tagNoOmitempty @@ -31,9 +32,6 @@ type ServerSettings interface { // @tg summary=`CreateRoom` CreateRoom(ctx context.Context, token string, createRoom types.CreateRoomRequest) (err error) // @tg http-headers=token|Token - // @tg summary=`GetGameResults` - GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) - // @tg http-headers=token|Token // @tg summary=`SetNotifyServer` SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) } diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/network.go b/vendor/github.com/ascenmmo/udp-server/pkg/api/types/network.go similarity index 51% rename from vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/network.go rename to vendor/github.com/ascenmmo/udp-server/pkg/api/types/network.go index 58ad4bb..8477730 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/network.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/api/types/network.go @@ -1,5 +1,4 @@ package types type CreateRoomRequest struct { - GameConfigs GameConfigs `json:"game_configs"` } diff --git a/vendor/github.com/ascenmmo/udp-server/internal/entities/room.go b/vendor/github.com/ascenmmo/udp-server/pkg/api/types/room.go similarity index 93% rename from vendor/github.com/ascenmmo/udp-server/internal/entities/room.go rename to vendor/github.com/ascenmmo/udp-server/pkg/api/types/room.go index d3bb2ab..a8bcef2 100644 --- a/vendor/github.com/ascenmmo/udp-server/internal/entities/room.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/api/types/room.go @@ -1,8 +1,7 @@ -package entities +package types import ( "github.com/ascenmmo/udp-server/internal/connection" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" "github.com/google/uuid" "sync" "time" @@ -16,8 +15,6 @@ type Room struct { Users []*User - GameConfigs types.GameConfigs - UpdatedAt time.Time mtx sync.RWMutex } diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/settings.go b/vendor/github.com/ascenmmo/udp-server/pkg/api/types/settings.go similarity index 82% rename from vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/settings.go rename to vendor/github.com/ascenmmo/udp-server/pkg/api/types/settings.go index ec7b61f..75311ad 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/settings.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/api/types/settings.go @@ -1,7 +1,6 @@ package types import ( - "fmt" "github.com/ascenmmo/udp-server/env" "runtime" ) @@ -32,9 +31,6 @@ func CountConnectionsMAX() int { runtime.ReadMemStats(&memStats) connections := calculateConnections(numCPUs, memStats.Sys) - fmt.Printf("Количество CPU: %d\n", numCPUs) - fmt.Printf("Объем оперативной памяти: %d MB\n", memStats.Sys/(1024*1024)) - fmt.Printf("Рекомендуемое количество соединений по UDP: %d\n", connections) return connections } diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-exchange.go b/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-exchange.go index 77e059a..ac05e52 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-exchange.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-exchange.go @@ -2,7 +2,7 @@ package udpGameServer import ( - "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/google/uuid" ) @@ -39,14 +39,6 @@ type requestServerSettingsCreateRoom struct { // Formal exchange type, please do not delete. type responseServerSettingsCreateRoom struct{} -type requestServerSettingsGetGameResults struct { - Token string `json:"token"` -} - -type responseServerSettingsGetGameResults struct { - GameConfigResults []types.GameConfigResults `json:"gameConfigResults"` -} - type requestServerSettingsSetNotifyServer struct { Token string `json:"token"` Id uuid.UUID `json:"id"` diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-fallback.go b/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-fallback.go index 8b466be..82c2e17 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-fallback.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-fallback.go @@ -6,6 +6,5 @@ type fallbackServerSettings interface { HealthCheck(err error) bool GetServerSettings(err error) bool CreateRoom(err error) bool - GetGameResults(err error) bool SetNotifyServer(err error) bool } diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-jsonrpc.go b/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-jsonrpc.go index b7581c3..ab50e08 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-jsonrpc.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/serversettings-jsonrpc.go @@ -4,9 +4,9 @@ package udpGameServer import ( "context" "fmt" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/hasher" "github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/jsonrpc" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" "github.com/google/uuid" ) @@ -18,7 +18,6 @@ type retServerSettingsGetConnectionsNum = func(countConn int, exists bool, err e type retServerSettingsHealthCheck = func(exists bool, err error) type retServerSettingsGetServerSettings = func(settings types.Settings, err error) type retServerSettingsCreateRoom = func(err error) -type retServerSettingsGetGameResults = func(gameConfigResults []types.GameConfigResults, err error) type retServerSettingsSetNotifyServer = func(err error) func (cli *ClientServerSettings) GetConnectionsNum(ctx context.Context, token string) (countConn int, exists bool, err error) { @@ -239,59 +238,6 @@ func (cli *ClientServerSettings) ReqCreateRoom(ctx context.Context, callback ret return } -func (cli *ClientServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - - request := requestServerSettingsGetGameResults{Token: token} - var response responseServerSettingsGetGameResults - var rpcResponse *jsonrpc.ResponseRPC - cacheKey, _ := hasher.Hash(request) - rpcResponse, err = cli.rpc.Call(ctx, "serversettings.getgameresults", request) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetGameResults - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - if err = cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response); err != nil { - return - } - return response.GameConfigResults, err -} - -func (cli *ClientServerSettings) ReqGetGameResults(ctx context.Context, callback retServerSettingsGetGameResults, token string) (request RequestRPC) { - - request = RequestRPC{rpcRequest: &jsonrpc.RequestRPC{ - ID: jsonrpc.NewID(), - JSONRPC: jsonrpc.Version, - Method: "serversettings.getgameresults", - Params: requestServerSettingsGetGameResults{Token: token}, - }} - if callback != nil { - var response responseServerSettingsGetGameResults - request.retHandler = func(err error, rpcResponse *jsonrpc.ResponseRPC) { - cacheKey, _ := hasher.Hash(request.rpcRequest.Params) - var fallbackCheck func(error) bool - if cli.fallbackServerSettings != nil { - fallbackCheck = cli.fallbackServerSettings.GetGameResults - } - if rpcResponse != nil && rpcResponse.Error != nil { - if cli.errorDecoder != nil { - err = cli.errorDecoder(rpcResponse.Error.Raw()) - } else { - err = fmt.Errorf(rpcResponse.Error.Message) - } - } - callback(response.GameConfigResults, cli.proceedResponse(ctx, err, cacheKey, fallbackCheck, rpcResponse, &response)) - } - } - return -} - func (cli *ClientServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { request := requestServerSettingsSetNotifyServer{ diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/game_config.go b/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/game_config.go deleted file mode 100644 index beba11e..0000000 --- a/vendor/github.com/ascenmmo/udp-server/pkg/restconnection/types/game_config.go +++ /dev/null @@ -1,45 +0,0 @@ -package types - -import ( - tokentype "github.com/ascenmmo/token-generator/token_type" - "github.com/google/uuid" -) - -type GameConfigExecutor interface { - Name() string - Execute(clientInfo tokentype.Info, config SortingConfig, newUserData interface{}, oldResult GameConfigResults) (newResult GameConfigResults, err error) -} - -type GameConfigResults struct { - GameID uuid.UUID `json:"game_id"` - RoomID uuid.UUID `json:"room_id"` - Result map[string]interface{} `json:"result"` -} - -type GameConfigs struct { - GameID uuid.UUID `json:"game_id" bson:"_id"` - SortingConfig []SortingConfig `json:"sorting_config" bson:"sorting_config"` - IsExists bool `json:"-"` -} - -type SortingConfig struct { - Name string `json:"name" bson:"name"` - Params []ParamMetadata `json:"params" bson:"params"` - UseOnServerType string `json:"use_on_server_type" bson:"use_on_server_type"` - ResultName string `json:"result_name" bson:"result_name"` - ResultType string `json:"result_type" bson:"result_type"` - Executor GameConfigExecutor `json:"-"` -} - -type ParamMetadata struct { - ColumnName string `json:"column_name" bson:"column_name"` - ValueType string `json:"value_type" bson:"value_type"` -} - -func (g *GameConfigs) RemoveSortingConfig(name string) { - for i, v := range g.SortingConfig { - if v.Name == name { - g.SortingConfig = append(g.SortingConfig[:i], g.SortingConfig[i+1:]...) - } - } -} diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/start/start.go b/vendor/github.com/ascenmmo/udp-server/pkg/start/start.go index 3bfbc5a..061851c 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/start/start.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/start/start.go @@ -7,17 +7,16 @@ import ( "github.com/ascenmmo/udp-server/internal/handler/tcp" "github.com/ascenmmo/udp-server/internal/handler/udp" "github.com/ascenmmo/udp-server/internal/service" - configsService "github.com/ascenmmo/udp-server/internal/service/configs_service" memoryDB "github.com/ascenmmo/udp-server/internal/storage" "github.com/ascenmmo/udp-server/internal/utils" "github.com/ascenmmo/udp-server/pkg/transport" "github.com/rs/zerolog" + "runtime" "time" ) -func StartUDP(ctx context.Context, address string, tcpPort, udpPort string, token string, udpRateLimit int, dataTTL, gameConfigResultsTTl time.Duration, logger zerolog.Logger) (err error) { +func StartUDP(ctx context.Context, address string, tcpPort, udpPort string, token string, udpRateLimit int, dataTTL time.Duration, logger zerolog.Logger, logWithMemoryUsage bool) (err error) { ramDB := memoryDB.NewMemoryDb(ctx, dataTTL) - gameConfigResultsDB := memoryDB.NewMemoryDb(ctx, gameConfigResultsTTl) rateLimitDB := memoryDB.NewMemoryDb(ctx, 1) tokenGen, err := tokengenerator.NewTokenGenerator(token) @@ -25,8 +24,7 @@ func StartUDP(ctx context.Context, address string, tcpPort, udpPort string, toke return err } - gameConfigsService := configsService.NewGameConfigsService(gameConfigResultsDB, tokenGen) - newService := service.NewService(tokenGen, ramDB, gameConfigsService, logger) + newService := service.NewService(tokenGen, ramDB, logger) errors := make(chan error) @@ -45,6 +43,10 @@ func StartUDP(ctx context.Context, address string, tcpPort, udpPort string, toke go newUDP.Sender(ctx) + if logWithMemoryUsage { + logMemoryUsage(logger) + } + go func() { serverSettings := tcp.NewServerSettings(utils.NewRateLimit(10, rateLimitDB), newService) @@ -65,3 +67,19 @@ func StartUDP(ctx context.Context, address string, tcpPort, udpPort string, toke return err } + +func logMemoryUsage(logger zerolog.Logger) { + ticker := time.NewTicker(time.Second * 10) + go func() { + for range ticker.C { + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + logger.Info(). + Interface("num cpu", runtime.NumCPU()). + Interface("Memory Usage", stats.Alloc/1024/1024). + Interface("TotalAlloc", stats.TotalAlloc/1024/1024). + Interface("Sys", stats.Sys/1024/1024). + Interface("NumGC", stats.NumGC) + } + }() +} diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/jsonrpc.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/jsonrpc.go index 5b8ca93..b5fb6db 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/jsonrpc.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/jsonrpc.go @@ -150,8 +150,6 @@ func (srv *Server) doSingleBatch(ctx *fiber.Ctx, request baseJsonRPC) (response return srv.httpServerSettings.getServerSettings(ctx, request) case "serversettings.createroom": return srv.httpServerSettings.createRoom(ctx, request) - case "serversettings.getgameresults": - return srv.httpServerSettings.getGameResults(ctx, request) case "serversettings.setnotifyserver": return srv.httpServerSettings.setNotifyServer(ctx, request) default: diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-exchange.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-exchange.go index 0ac78e5..3a653fc 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-exchange.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-exchange.go @@ -2,7 +2,7 @@ package transport import ( - "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/google/uuid" ) @@ -39,14 +39,6 @@ type requestServerSettingsCreateRoom struct { // Formal exchange type, please do not delete. type responseServerSettingsCreateRoom struct{} -type requestServerSettingsGetGameResults struct { - Token string `json:"token"` -} - -type responseServerSettingsGetGameResults struct { - GameConfigResults []types.GameConfigResults `json:"gameConfigResults"` -} - type requestServerSettingsSetNotifyServer struct { Token string `json:"token"` Id uuid.UUID `json:"id"` diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-http.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-http.go index 8cf9b06..234aa57 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-http.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-http.go @@ -2,7 +2,7 @@ package transport import ( - "github.com/ascenmmo/udp-server/pkg/restconnection" + "github.com/ascenmmo/udp-server/pkg/api" "github.com/gofiber/fiber/v2" ) @@ -11,10 +11,10 @@ type httpServerSettings struct { maxBatchSize int maxParallelBatch int svc *serverServerSettings - base restconnection.ServerSettings + base api.ServerSettings } -func NewServerSettings(svcServerSettings restconnection.ServerSettings) (srv *httpServerSettings) { +func NewServerSettings(svcServerSettings api.ServerSettings) (srv *httpServerSettings) { srv = &httpServerSettings{ base: svcServerSettings, @@ -48,6 +48,5 @@ func (http *httpServerSettings) SetRoutes(route *fiber.App) { route.Post("/api/v1/udp/serverSettings/healthCheck", http.serveHealthCheck) route.Post("/api/v1/udp/serverSettings/getServerSettings", http.serveGetServerSettings) route.Post("/api/v1/udp/serverSettings/createRoom", http.serveCreateRoom) - route.Post("/api/v1/udp/serverSettings/getGameResults", http.serveGetGameResults) route.Post("/api/v1/udp/serverSettings/setNotifyServer", http.serveSetNotifyServer) } diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-jsonrpc.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-jsonrpc.go index ae45b35..5726aca 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-jsonrpc.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-jsonrpc.go @@ -238,63 +238,6 @@ func (http *httpServerSettings) createRoom(ctx *fiber.Ctx, requestBase baseJsonR } return } -func (http *httpServerSettings) serveGetGameResults(ctx *fiber.Ctx) (err error) { - return http.serveMethod(ctx, "getgameresults", http.getGameResults) -} -func (http *httpServerSettings) getGameResults(ctx *fiber.Ctx, requestBase baseJsonRPC) (responseBase *baseJsonRPC) { - - var err error - var request requestServerSettingsGetGameResults - - methodCtx := ctx.UserContext() - span := otg.SpanFromContext(methodCtx) - span.SetTag("method", "getGameResults") - - if requestBase.Params != nil { - if err = json.Unmarshal(requestBase.Params, &request); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "request body could not be decoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "request body could not be decoded: "+err.Error(), nil) - } - } - if requestBase.Version != Version { - ext.Error.Set(span, true) - span.SetTag("msg", "incorrect protocol version: "+requestBase.Version) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "incorrect protocol version: "+requestBase.Version, nil) - } - - if _token := string(ctx.Request().Header.Peek("Token")); _token != "" { - var token string - token = _token - request.Token = token - } - - var response responseServerSettingsGetGameResults - response.GameConfigResults, err = http.svc.GetGameResults(methodCtx, request.Token) - if err != nil { - if http.errorHandler != nil { - err = http.errorHandler(err) - } - ext.Error.Set(span, true) - span.SetTag("msg", err) - span.SetTag("errData", toString(err)) - code := internalError - if errCoder, ok := err.(withErrorCode); ok { - code = errCoder.Code() - } - return makeErrorResponseJsonRPC(requestBase.ID, code, err.Error(), err) - } - responseBase = &baseJsonRPC{ - ID: requestBase.ID, - Version: Version, - } - if responseBase.Result, err = json.Marshal(response); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "response body could not be encoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "response body could not be encoded: "+err.Error(), nil) - } - return -} func (http *httpServerSettings) serveSetNotifyServer(ctx *fiber.Ctx) (err error) { return http.serveMethod(ctx, "setnotifyserver", http.setNotifyServer) } @@ -475,8 +418,6 @@ func (http *httpServerSettings) doSingleBatch(ctx *fiber.Ctx, request baseJsonRP return http.getServerSettings(ctx, request) case "createroom": return http.createRoom(ctx, request) - case "getgameresults": - return http.getGameResults(ctx, request) case "setnotifyserver": return http.setNotifyServer(ctx, request) default: diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-logger.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-logger.go index d0ff1fa..ca82b0b 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-logger.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-logger.go @@ -3,8 +3,8 @@ package transport import ( "context" - "github.com/ascenmmo/udp-server/pkg/restconnection" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/ascenmmo/udp-server/pkg/transport/viewer" "github.com/google/uuid" "github.com/rs/zerolog" @@ -13,11 +13,11 @@ import ( ) type loggerServerSettings struct { - next restconnection.ServerSettings + next api.ServerSettings } func loggerMiddlewareServerSettings() MiddlewareServerSettings { - return func(next restconnection.ServerSettings) restconnection.ServerSettings { + return func(next api.ServerSettings) api.ServerSettings { return &loggerServerSettings{next: next} } } @@ -104,25 +104,6 @@ func (m loggerServerSettings) CreateRoom(ctx context.Context, token string, crea return m.next.CreateRoom(ctx, token, createRoom) } -func (m loggerServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - logger := log.Ctx(ctx).With().Str("service", "ServerSettings").Str("method", "getGameResults").Logger() - defer func(begin time.Time) { - logHandle := func(ev *zerolog.Event) { - fields := map[string]interface{}{ - "request": viewer.Sprintf("%+v", requestServerSettingsGetGameResults{Token: token}), - "response": viewer.Sprintf("%+v", responseServerSettingsGetGameResults{GameConfigResults: gameConfigResults}), - } - ev.Fields(fields).Str("took", time.Since(begin).String()) - } - if err != nil { - logger.Error().Err(err).Func(logHandle).Msg("call getGameResults") - return - } - logger.Info().Func(logHandle).Msg("call getGameResults") - }(time.Now()) - return m.next.GetGameResults(ctx, token) -} - func (m loggerServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { logger := log.Ctx(ctx).With().Str("service", "ServerSettings").Str("method", "setNotifyServer").Logger() defer func(begin time.Time) { diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-middleware.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-middleware.go index 065351e..179ee06 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-middleware.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-middleware.go @@ -3,8 +3,8 @@ package transport import ( "context" - "github.com/ascenmmo/udp-server/pkg/restconnection" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/google/uuid" ) @@ -12,14 +12,12 @@ type ServerSettingsGetConnectionsNum func(ctx context.Context, token string) (co type ServerSettingsHealthCheck func(ctx context.Context, token string) (exists bool, err error) type ServerSettingsGetServerSettings func(ctx context.Context, token string) (settings types.Settings, err error) type ServerSettingsCreateRoom func(ctx context.Context, token string, createRoom types.CreateRoomRequest) (err error) -type ServerSettingsGetGameResults func(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) type ServerSettingsSetNotifyServer func(ctx context.Context, token string, id uuid.UUID, url string) (err error) -type MiddlewareServerSettings func(next restconnection.ServerSettings) restconnection.ServerSettings +type MiddlewareServerSettings func(next api.ServerSettings) api.ServerSettings type MiddlewareServerSettingsGetConnectionsNum func(next ServerSettingsGetConnectionsNum) ServerSettingsGetConnectionsNum type MiddlewareServerSettingsHealthCheck func(next ServerSettingsHealthCheck) ServerSettingsHealthCheck type MiddlewareServerSettingsGetServerSettings func(next ServerSettingsGetServerSettings) ServerSettingsGetServerSettings type MiddlewareServerSettingsCreateRoom func(next ServerSettingsCreateRoom) ServerSettingsCreateRoom -type MiddlewareServerSettingsGetGameResults func(next ServerSettingsGetGameResults) ServerSettingsGetGameResults type MiddlewareServerSettingsSetNotifyServer func(next ServerSettingsSetNotifyServer) ServerSettingsSetNotifyServer diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-server.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-server.go index 2bdea55..2f6e11f 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-server.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-server.go @@ -3,18 +3,17 @@ package transport import ( "context" - "github.com/ascenmmo/udp-server/pkg/restconnection" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/google/uuid" ) type serverServerSettings struct { - svc restconnection.ServerSettings + svc api.ServerSettings getConnectionsNum ServerSettingsGetConnectionsNum healthCheck ServerSettingsHealthCheck getServerSettings ServerSettingsGetServerSettings createRoom ServerSettingsCreateRoom - getGameResults ServerSettingsGetGameResults setNotifyServer ServerSettingsSetNotifyServer } @@ -24,18 +23,16 @@ type MiddlewareSetServerSettings interface { WrapHealthCheck(m MiddlewareServerSettingsHealthCheck) WrapGetServerSettings(m MiddlewareServerSettingsGetServerSettings) WrapCreateRoom(m MiddlewareServerSettingsCreateRoom) - WrapGetGameResults(m MiddlewareServerSettingsGetGameResults) WrapSetNotifyServer(m MiddlewareServerSettingsSetNotifyServer) WithTrace() WithLog() } -func newServerServerSettings(svc restconnection.ServerSettings) *serverServerSettings { +func newServerServerSettings(svc api.ServerSettings) *serverServerSettings { return &serverServerSettings{ createRoom: svc.CreateRoom, getConnectionsNum: svc.GetConnectionsNum, - getGameResults: svc.GetGameResults, getServerSettings: svc.GetServerSettings, healthCheck: svc.HealthCheck, setNotifyServer: svc.SetNotifyServer, @@ -49,7 +46,6 @@ func (srv *serverServerSettings) Wrap(m MiddlewareServerSettings) { srv.healthCheck = srv.svc.HealthCheck srv.getServerSettings = srv.svc.GetServerSettings srv.createRoom = srv.svc.CreateRoom - srv.getGameResults = srv.svc.GetGameResults srv.setNotifyServer = srv.svc.SetNotifyServer } @@ -69,10 +65,6 @@ func (srv *serverServerSettings) CreateRoom(ctx context.Context, token string, c return srv.createRoom(ctx, token, createRoom) } -func (srv *serverServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - return srv.getGameResults(ctx, token) -} - func (srv *serverServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { return srv.setNotifyServer(ctx, token, id, url) } @@ -93,10 +85,6 @@ func (srv *serverServerSettings) WrapCreateRoom(m MiddlewareServerSettingsCreate srv.createRoom = m(srv.createRoom) } -func (srv *serverServerSettings) WrapGetGameResults(m MiddlewareServerSettingsGetGameResults) { - srv.getGameResults = m(srv.getGameResults) -} - func (srv *serverServerSettings) WrapSetNotifyServer(m MiddlewareServerSettingsSetNotifyServer) { srv.setNotifyServer = m(srv.setNotifyServer) } diff --git a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-trace.go b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-trace.go index 4325e4a..18a0c0b 100644 --- a/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-trace.go +++ b/vendor/github.com/ascenmmo/udp-server/pkg/transport/serversettings-trace.go @@ -3,17 +3,17 @@ package transport import ( "context" - "github.com/ascenmmo/udp-server/pkg/restconnection" - "github.com/ascenmmo/udp-server/pkg/restconnection/types" + "github.com/ascenmmo/udp-server/pkg/api" + "github.com/ascenmmo/udp-server/pkg/api/types" "github.com/google/uuid" "github.com/opentracing/opentracing-go" ) type traceServerSettings struct { - next restconnection.ServerSettings + next api.ServerSettings } -func traceMiddlewareServerSettings(next restconnection.ServerSettings) restconnection.ServerSettings { +func traceMiddlewareServerSettings(next api.ServerSettings) api.ServerSettings { return &traceServerSettings{next: next} } @@ -41,12 +41,6 @@ func (svc traceServerSettings) CreateRoom(ctx context.Context, token string, cre return svc.next.CreateRoom(ctx, token, createRoom) } -func (svc traceServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - span := opentracing.SpanFromContext(ctx) - span.SetTag("method", "GetGameResults") - return svc.next.GetGameResults(ctx, token) -} - func (svc traceServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { span := opentracing.SpanFromContext(ctx) span.SetTag("method", "SetNotifyServer") diff --git a/vendor/github.com/ascenmmo/websocket-server/LICENSE b/vendor/github.com/ascenmmo/websocket-server/LICENSE index 919116d..b2f3011 100644 --- a/vendor/github.com/ascenmmo/websocket-server/LICENSE +++ b/vendor/github.com/ascenmmo/websocket-server/LICENSE @@ -1,15 +1,21 @@ -Copyright (C) 2024 ascenmmo +MIT License + +Copyright (c) 2024 Temur Abdurakhmanov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -in the Software for personal, non-commercial use only, including the rights -to use, copy, modify, and merge the Software, subject to the following conditions: +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The Software may not be used for commercial purposes, including, but not limited to: -- Hosting or selling services based on this Software -- Redistributing the Software as part of a paid service +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY. \ No newline at end of file +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ascenmmo/websocket-server/env/env.go b/vendor/github.com/ascenmmo/websocket-server/env/env.go deleted file mode 100644 index d86a206..0000000 --- a/vendor/github.com/ascenmmo/websocket-server/env/env.go +++ /dev/null @@ -1,9 +0,0 @@ -package env - -var ( - ServerAddress = "0.0.0.0" - TCPPort = "8082" - WebsocketPort = "4240" - TokenKey = "_remember_token_mast_be_32_bytes" - MaxRequestPerSecond = 50 -) diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go b/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go index 4a557a1..313a7a5 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go @@ -3,4 +3,5 @@ package connection type DataSender interface { Write([]byte) error GetID() string + Close() } diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/connection/notify_servers.go b/vendor/github.com/ascenmmo/websocket-server/internal/connection/notify_servers.go deleted file mode 100644 index 0d3853f..0000000 --- a/vendor/github.com/ascenmmo/websocket-server/internal/connection/notify_servers.go +++ /dev/null @@ -1,97 +0,0 @@ -package connection - -import ( - "encoding/json" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" - "github.com/google/uuid" - "github.com/gorilla/websocket" - "net/http" -) - -type NotifyServers interface { - NotifyServers(ids []uuid.UUID, req types.Request) error - AddServer(ID uuid.UUID, token string, addr string) error -} - -type notifier struct { - servers []*server -} - -func NewNotifierServers() NotifyServers { - return ¬ifier{} -} - -type server struct { - ID uuid.UUID - Addr string - Connection *websocket.Conn -} - -func (n *notifier) NotifyServers(ids []uuid.UUID, req types.Request) error { - if len(n.servers) == 0 { - return nil - } - marshal, err := json.Marshal(req) - if err != nil { - return err - } - for _, id := range ids { - for i, server := range n.servers { - if server.ID == id { - err = n.servers[i].Connection.WriteMessage(websocket.BinaryMessage, marshal) - if err != nil { - err = n.servers[i].Connect(req.Token) - if err != nil { - n.RemoveNotifyServer(id) - return err - } - return n.servers[i].Connection.WriteMessage(websocket.BinaryMessage, marshal) - } - } - } - } - return nil -} - -func (n *notifier) AddServer(ID uuid.UUID, token string, addr string) error { - newServer := &server{ - ID: ID, - Addr: addr, - } - err := newServer.Connect(token) - if err != nil { - return err - } - for i, s := range n.servers { - if s.ID == ID { - n.servers[i] = newServer - return nil - } - } - n.servers = append(n.servers, newServer) - return nil -} - -func (n *notifier) RemoveNotifyServer(id uuid.UUID) { - for i, s := range n.servers { - if s.ID == id { - _ = n.servers[i].Connection.Close() - n.servers = append(n.servers[:i], n.servers[i+1:]...) - } - } -} - -func (s *server) Connect(token string) error { - url := s.Addr + "/api/ws/connect" - - headers := http.Header{} - headers.Add("token", token) - - conn, _, err := websocket.DefaultDialer.Dial(url, headers) - if err != nil { - return err - } - s.Connection = conn - - return nil -} diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go b/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go index 151cd4f..0ae3800 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go @@ -1,9 +1,13 @@ package connection -import "github.com/gorilla/websocket" +import ( + "context" + "github.com/gorilla/websocket" +) type WebSocketConnection struct { - Conn *websocket.Conn + Conn *websocket.Conn + CtxClose context.CancelFunc } func (u *WebSocketConnection) GetID() string { @@ -15,3 +19,9 @@ func (u *WebSocketConnection) Write(msg []byte) error { return err } + +func (u *WebSocketConnection) Close() { + if u.CtxClose != nil { + u.CtxClose() + } +} diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/handler/tcp/server_settings.go b/vendor/github.com/ascenmmo/websocket-server/internal/handler/tcp/server_settings.go index e14d9c5..2257900 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/handler/tcp/server_settings.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/handler/tcp/server_settings.go @@ -4,9 +4,8 @@ import ( "context" "github.com/ascenmmo/websocket-server/internal/service" "github.com/ascenmmo/websocket-server/internal/utils" + "github.com/ascenmmo/websocket-server/pkg/api/types" "github.com/ascenmmo/websocket-server/pkg/errors" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" - "github.com/google/uuid" ) type ServerSettings struct { @@ -45,25 +44,7 @@ func (r *ServerSettings) CreateRoom(ctx context.Context, token string, createRoo if limited { return errors.ErrTooManyRequests } - err = r.server.CreateRoom(token, createRoom.GameConfigs) - return -} - -func (r *ServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - limited := r.rateLimit.IsLimited(token) - if limited { - return gameConfigResults, errors.ErrTooManyRequests - } - gameConfigResults, err = r.server.GetGameResults(token) - return -} - -func (r *ServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - limited := r.rateLimit.IsLimited(token) - if limited { - return errors.ErrTooManyRequests - } - err = r.server.SetRoomNotifyServer(token, id, url) + err = r.server.CreateRoom(token) return } diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go b/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go index 94779b1..1b1bbd7 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go @@ -2,13 +2,11 @@ package wsconnection import ( "context" - "encoding/json" "fmt" tokentype "github.com/ascenmmo/token-generator/token_type" "github.com/ascenmmo/websocket-server/internal/connection" "github.com/ascenmmo/websocket-server/internal/service" "github.com/ascenmmo/websocket-server/internal/utils" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/rs/zerolog" @@ -20,8 +18,7 @@ import ( const ( bufferSize = 32 * 1024 readTimeout = 60 * time.Second - writeTimeout = 10 * time.Second - pingInterval = 30 * time.Second + pingInterval = 10 * time.Second ) var upgrader = websocket.Upgrader{ @@ -33,12 +30,11 @@ var upgrader = websocket.Upgrader{ } type WebSocket struct { - server *http.Server - mtx sync.RWMutex - service service.Service - rateLimit utils.RateLimit - rateLimitBadMsg utils.RateLimit - logger zerolog.Logger + server *http.Server + mtx sync.RWMutex + service service.Service + rateLimit utils.RateLimit + logger zerolog.Logger } func (ws *WebSocket) connect(w http.ResponseWriter, req *http.Request) { @@ -56,7 +52,7 @@ func (ws *WebSocket) connect(w http.ResponseWriter, req *http.Request) { conn, err := upgrader.Upgrade(w, req, nil) if err != nil { - ws.logger.Error().Err(err).Msg("Failed to upgrade connection") + ws.logger.Error().Err(err).Msg("failed to upgrade connection") return } @@ -66,45 +62,46 @@ func (ws *WebSocket) connect(w http.ResponseWriter, req *http.Request) { func (ws *WebSocket) handleConnection(ctx context.Context, token string, clientInfo tokentype.Info, conn *websocket.Conn) { defer func() { conn.Close() - err := ws.service.RemoveUser(token, clientInfo.UserID) + err := ws.service.RemoveUser(clientInfo, clientInfo.UserID) if err != nil { - ws.logger.Error().Err(err).Msg("Failed to remove user") + ws.logger.Error().Err(err).Msg("failed to remove user") } }() conn.SetReadLimit(bufferSize) err := conn.SetReadDeadline(time.Now().Add(readTimeout)) if err != nil { - ws.logger.Error().Err(err).Msg("Failed to set read deadline") + ws.logger.Error().Err(err).Msg("failed to set read deadline") fmt.Println(err) } err = ws.service.SetNewConnection(clientInfo, connection.DataSender(&connection.WebSocketConnection{Conn: conn})) if err != nil { - ws.logger.Error().Err(err).Msg("Failed to set new connection") - fmt.Println(err) + ws.logger.Error().Err(err).Msg("failed to set new connection") } pingTicker := time.NewTicker(pingInterval) defer pingTicker.Stop() + ctx, cancel := context.WithCancel(ctx) + for { select { case <-ctx.Done(): return case <-pingTicker.C: if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { - ws.logger.Error().Err(err).Msg("Ping failed") + ws.logger.Error().Err(err).Msg("ping failed") return } default: messageType, message, err := conn.ReadMessage() if err != nil { if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { - ws.logger.Info().Msg("Client disconnected normally") - } else { - ws.logger.Error().Err(err).Msg("ReadMessage failed") + ws.logger.Error().Msg("client disconnected normally") + return } + ws.logger.Error().Err(err).Msg("readMessage failed") return } @@ -117,45 +114,24 @@ func (ws *WebSocket) handleConnection(ctx context.Context, token string, clientI } if ws.rateLimit.IsLimited(token) { - ws.logger.Warn().Msg("Rate limit exceeded") - continue - } - - var request types.Request - if err := json.Unmarshal(message, &request); err != nil { - if ws.rateLimitBadMsg.IsLimited(token) { - ws.logger.Warn().Msg("Rate limit exceeded for bad message formats") - return - } + ws.logger.Warn().Msg("rate limit exceeded") continue } - ds := connection.DataSender(&connection.WebSocketConnection{Conn: conn}) - - if request.Server != nil { - clientInfo, err = ws.service.ParseToken(request.Token) - if err != nil { - ws.logger.Error().Err(err).Msg("Failed to parse token") - return - } - } - - if request.Token != "" { - request.Token = token - } - users, msg, err := ws.service.GetUsersAndMessage(ds, clientInfo, request) + ds := connection.DataSender(&connection.WebSocketConnection{Conn: conn, CtxClose: cancel}) + users, newMessage, err := ws.service.GetUsersAndMessage(ds, clientInfo, message) if err != nil { if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { - ws.logger.Error().Err(err).Msg("Failed to send error message to client") + ws.logger.Error().Err(err).Msg("failed to send error message to client") } continue } for _, user := range users { - if err := user.Connection.Write(msg); err != nil { - ws.logger.Error().Err(err).Msg("Failed to send message to user") - if err := ws.service.RemoveUser(token, user.ID); err != nil { - ws.logger.Error().Err(err).Msg("Failed to remove user") + if err := user.Connection.Write(newMessage); err != nil { + ws.logger.Error().Err(err).Msg("failed to send message to user") + if err := ws.service.RemoveUser(clientInfo, user.ID); err != nil { + ws.logger.Error().Err(err).Msg("failed to remove user") } } } @@ -175,12 +151,11 @@ func (ws *WebSocket) Run(addr string) error { return ws.server.ListenAndServe() } -func NewWebSocket(service service.Service, rateLimit, rateLimitBadMsg utils.RateLimit, logger zerolog.Logger) *WebSocket { +func NewWebSocket(service service.Service, rateLimit utils.RateLimit, logger zerolog.Logger) *WebSocket { return &WebSocket{ - service: service, - rateLimit: rateLimit, - rateLimitBadMsg: rateLimitBadMsg, - mtx: sync.RWMutex{}, - logger: logger, + service: service, + rateLimit: rateLimit, + mtx: sync.RWMutex{}, + logger: logger, } } diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/count_results.go b/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/count_results.go deleted file mode 100644 index 3b6ec86..0000000 --- a/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/count_results.go +++ /dev/null @@ -1,191 +0,0 @@ -package configsService - -import ( - tokentype "github.com/ascenmmo/token-generator/token_type" - "github.com/ascenmmo/websocket-server/pkg/errors" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" -) - -func GetCountingFunctions() (functions []types.GameConfigExecutor) { - functions = []types.GameConfigExecutor{ - &IncrementResult{}, - &DecrementResult{}, - &AdditionDataResultToOld{}, - &SubtractDataResultToOld{}, - } - return functions -} - -type IncrementResult struct { -} - -func (c *IncrementResult) Name() string { - return "IncrementResult" -} - -func (c *IncrementResult) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(oldResult.Result) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - oldResult.Result[config.ResultName] = incrementValue(oldValue) - - return oldResult, nil -} - -type DecrementResult struct { -} - -func (c *DecrementResult) Name() string { - return "DecrementResult" -} - -func (c *DecrementResult) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(oldResult.Result) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - oldResult.Result[config.ResultName] = decrementValue(oldValue) - - return oldResult, nil -} - -type AdditionDataResultToOld struct{} - -func (c *AdditionDataResultToOld) Name() string { - return "AdditionDataResultToOld" -} - -func (c *AdditionDataResultToOld) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - for _, newValue := range values { - oldValue = additionValues(oldValue, newValue, config.ResultType) - } - - oldResult.Result[config.ResultName] = oldValue - - return oldResult, nil -} - -type SubtractDataResultToOld struct{} - -func (c *SubtractDataResultToOld) Name() string { - return "SubtractDataResultToOld" -} - -func (c *SubtractDataResultToOld) Execute(clientInfo tokentype.Info, config types.SortingConfig, newUserData interface{}, oldResult types.GameConfigResults) (newResult types.GameConfigResults, err error) { - data, err := parseUserData(newUserData) - if err != nil { - return oldResult, errors.ErrGameConfigMarshalUserData - } - - if len(oldResult.Result) == 0 { - oldResult.Result = make(map[string]interface{}) - } - - var values []interface{} - for _, v := range config.Params { - value, ok := data[v.ColumnName] - if !ok { - continue - } - values = append(values, value) - } - - if len(values) != len(config.Params) { - return oldResult, nil - } - - oldValue, ok := oldResult.Result[config.ResultName] - if !ok { - oldValue = 0 - } - - for _, newValue := range values { - oldValue = subtractValues(oldValue, newValue, config.ResultType) - } - - oldResult.Result[config.ResultName] = oldValue - - return oldResult, nil -} - -func parseUserData(newUserData interface{}) (map[string]interface{}, error) { - data, ok := newUserData.(map[string]interface{}) - if !ok { - return nil, errors.ErrGameConfigMarshalUserData - } - return data, nil -} diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/game_configs.go b/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/game_configs.go deleted file mode 100644 index 00f18cd..0000000 --- a/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/game_configs.go +++ /dev/null @@ -1,117 +0,0 @@ -package configsService - -import ( - tokengenerator "github.com/ascenmmo/token-generator/token_generator" - tokentype "github.com/ascenmmo/token-generator/token_type" - memoryDB "github.com/ascenmmo/websocket-server/internal/storage" - "github.com/ascenmmo/websocket-server/internal/utils" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" -) - -type GameConfigsService interface { - Do(token string, clientInfo tokentype.Info, gameConfig types.GameConfigs, data interface{}) - GetDeletedRoomsResults(clientInfo tokentype.Info, onlinePlayersTokens []string) (results []types.GameConfigResults, ok bool) - SetServerExecuteToGameConfig(clientInfo tokentype.Info, gameConfig types.GameConfigs) (newGameConfig types.GameConfigs) -} - -type gameConfig struct { - allGameConfigExecutor []types.GameConfigExecutor - token tokengenerator.TokenGenerator - storage memoryDB.IMemoryDB -} - -func (g *gameConfig) Do(token string, clientInfo tokentype.Info, gameConfig types.GameConfigs, data interface{}) { - if gameConfig.IsExists { - return - } - results, _ := g.getOldResults(clientInfo) - for _, sorting := range gameConfig.SortingConfig { - if sorting.Executor == nil { - continue - } - newResult, err := sorting.Executor.Execute(clientInfo, sorting, data, results) - if err != nil { - continue - } - results = newResult - } - g.storage.AddConnection(token) - g.serOldResults(clientInfo, results) -} - -func (g *gameConfig) GetDeletedRoomsResults(_ tokentype.Info, onlinePlayersTokens []string) (results []types.GameConfigResults, ok bool) { - ids := g.storage.GetAllConnection() - - uniqueTokens := make(map[string]struct{}, len(onlinePlayersTokens)) - for _, token := range onlinePlayersTokens { - uniqueTokens[token] = struct{}{} - } - - var notFoundTokens []string - for _, token := range ids { - if _, exists := uniqueTokens[token]; !exists { - notFoundTokens = append(notFoundTokens, token) - } - } - - clientsInfo := make(map[string]tokentype.Info) - for _, token := range onlinePlayersTokens { - info, err := g.token.ParseToken(token) - if err != nil { - continue - } - clientsInfo[info.RoomID.String()] = info - } - - for _, info := range clientsInfo { - configResults, ok := g.getOldResults(info) - if !ok { - continue - } - results = append(results, configResults) - } - - return results, len(results) > 0 -} - -func (g *gameConfig) SetServerExecuteToGameConfig(_ tokentype.Info, gameConfig types.GameConfigs) (newGameConfig types.GameConfigs) { - isConfigExecutorFound := false - for i, conf := range gameConfig.SortingConfig { - for _, executor := range g.allGameConfigExecutor { - if executor.Name() == conf.Name { - gameConfig.SortingConfig[i].Executor = executor - isConfigExecutorFound = true - } - } - } - gameConfig.IsExists = isConfigExecutorFound - return gameConfig -} - -func (g *gameConfig) getOldResults(clientInfo tokentype.Info) (configResults types.GameConfigResults, ok bool) { - key := utils.GenerateRoomKey(clientInfo) - data, ok := g.storage.GetData(key) - if ok { - if configResults, ok = data.(types.GameConfigResults); ok { - return configResults, true - } - } - return types.GameConfigResults{ - GameID: clientInfo.GameID, - RoomID: clientInfo.RoomID, - Result: make(map[string]interface{}), - }, false -} - -func (g *gameConfig) serOldResults(clientInfo tokentype.Info, configResults types.GameConfigResults) { - key := utils.GenerateRoomKey(clientInfo) - g.storage.SetData(key, configResults) -} - -func getAllFunctions() []types.GameConfigExecutor { - return GetCountingFunctions() -} - -func NewGameConfigsService(storage memoryDB.IMemoryDB, token tokengenerator.TokenGenerator) GameConfigsService { - return &gameConfig{allGameConfigExecutor: getAllFunctions(), storage: storage, token: token} -} diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/numbers.go b/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/numbers.go deleted file mode 100644 index 34bf68a..0000000 --- a/vendor/github.com/ascenmmo/websocket-server/internal/service/configs_service/numbers.go +++ /dev/null @@ -1,78 +0,0 @@ -package configsService - -func incrementValue(value any) any { - switch v := value.(type) { - case int: - return v + 1 - case int32: - return v + 1 - case int64: - return v + 1 - case float32: - return v + 1 - case float64: - return v + 1 - default: - return value - } -} - -func decrementValue(value any) any { - switch v := value.(type) { - case int: - return v - 1 - case int32: - return v - 1 - case int64: - return v - 1 - case float32: - return v - 1 - case float64: - return v - 1 - default: - return value - } -} - -func additionValues(v1, v2 any, expectedType string) any { - switch expectedType { - case "int": - return int(toFloat64(v1) + toFloat64(v2)) - case "int64": - return int64(toFloat64(v1) + toFloat64(v2)) - case "float64": - return toFloat64(v1) + toFloat64(v2) - default: - return nil // В случае неподдерживаемого типа - } -} - -func subtractValues(v1, v2 any, expectedType string) any { - switch expectedType { - case "int": - return int(toFloat64(v1) - toFloat64(v2)) - case "int64": - return int64(toFloat64(v1) + toFloat64(v2)) - case "float64": - return toFloat64(v1) - toFloat64(v2) - default: - return nil // В случае неподдерживаемого типа - } -} - -func toFloat64(value any) float64 { - switch v := value.(type) { - case int: - return float64(v) - case int32: - return float64(v) - case int64: - return float64(v) - case float32: - return float64(v) - case float64: - return v - default: - return 0 - } -} diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go b/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go index 846a390..a7e5581 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go @@ -1,17 +1,14 @@ package service import ( - "encoding/json" "fmt" tokengenerator "github.com/ascenmmo/token-generator/token_generator" tokentype "github.com/ascenmmo/token-generator/token_type" "github.com/ascenmmo/websocket-server/internal/connection" - configsService "github.com/ascenmmo/websocket-server/internal/service/configs_service" "github.com/ascenmmo/websocket-server/internal/storage" "github.com/ascenmmo/websocket-server/internal/utils" - "github.com/ascenmmo/websocket-server/pkg/entities" + "github.com/ascenmmo/websocket-server/pkg/api/types" "github.com/ascenmmo/websocket-server/pkg/errors" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" "github.com/google/uuid" "github.com/rs/zerolog" "runtime" @@ -20,21 +17,17 @@ import ( type Service interface { GetConnectionsNum() (countConn int, exists bool) - CreateRoom(token string, configs types.GameConfigs) error - SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) - NotifyAllServers(clientInfo tokentype.Info, req types.Request) (err error) - GetUsersAndMessage(ds connection.DataSender, clientInfo tokentype.Info, req types.Request) (users []entities.User, msg []byte, err error) - RemoveUser(token string, userID uuid.UUID) (err error) + CreateRoom(token string) error + GetUsersAndMessage(ds connection.DataSender, clientInfo tokentype.Info, req []byte) (users []types.User, msg []byte, err error) + RemoveUser(clientInfo tokentype.Info, userID uuid.UUID) (err error) ParseToken(token string) (info tokentype.Info, err error) SetNewConnection(clientInfo tokentype.Info, ds connection.DataSender) (err error) - GetGameResults(token string) (results []types.GameConfigResults, err error) } type service struct { - maxConnections uint64 - gameConfigService configsService.GameConfigsService - storage memoryDB.IMemoryDB - token tokengenerator.TokenGenerator + maxConnections uint64 + storage memoryDB.IMemoryDB + token tokengenerator.TokenGenerator logger zerolog.Logger } @@ -49,66 +42,7 @@ func (s *service) GetConnectionsNum() (countConn int, exists bool) { return count, true } -func (s *service) SetRoomNotifyServer(token string, id uuid.UUID, url string) (err error) { - clientInfo, err := s.token.ParseToken(token) - if err != nil { - return err - } - - room, err := s.getRoom(clientInfo) - if err != nil { - return err - } - - room.SetServerID(id) - - data, _ := s.storage.GetData(utils.GenerateNotifyServerKey()) - - server, ok := data.(connection.NotifyServers) - if !ok { - s.logger.Warn().Msg("NotifyServers cant get interfase") - server = connection.NewNotifierServers() - } - - err = server.AddServer(id, token, url) - if err != nil { - return err - } - - s.storage.SetData(utils.GenerateNotifyServerKey(), server) - - return nil - -} - -func (s *service) NotifyAllServers(clientInfo tokentype.Info, req types.Request) (err error) { - room, err := s.getRoom(clientInfo) - if err != nil { - return err - } - if len(room.ServerID) == 0 { - return nil - } - - data, ok := s.storage.GetData(utils.GenerateNotifyServerKey()) - if !ok { - return errors.ErrNotifyServerNotFound - } - - servers, ok := data.(connection.NotifyServers) - if !ok { - return errors.ErrNotifyServerNotValid - } - - err = servers.NotifyServers(room.ServerID, req) - if err != nil { - return err - } - - return nil -} - -func (s *service) CreateRoom(token string, configs types.GameConfigs) error { +func (s *service) CreateRoom(token string) error { clientInfo, err := s.token.ParseToken(token) if err != nil { return err @@ -121,18 +55,15 @@ func (s *service) CreateRoom(token string, configs types.GameConfigs) error { return errors.ErrRoomIsExists } - configs = s.gameConfigService.SetServerExecuteToGameConfig(clientInfo, configs) - - s.setRoom(clientInfo, &entities.Room{ - GameID: clientInfo.GameID, - RoomID: clientInfo.RoomID, - GameConfigs: configs, + s.setRoom(clientInfo, &types.Room{ + GameID: clientInfo.GameID, + RoomID: clientInfo.RoomID, }) return nil } -func (s *service) GetUsersAndMessage(ds connection.DataSender, clientInfo tokentype.Info, req types.Request) (users []entities.User, msg []byte, err error) { +func (s *service) GetUsersAndMessage(ds connection.DataSender, clientInfo tokentype.Info, req []byte) (users []types.User, msg []byte, err error) { room, err := s.getRoom(clientInfo) if err != nil { return nil, nil, err @@ -151,7 +82,7 @@ func (s *service) GetUsersAndMessage(ds connection.DataSender, clientInfo tokent } if isNew { - room.SetUser(&entities.User{ + room.SetUser(&types.User{ ID: clientInfo.UserID, Connection: ds, }) @@ -159,37 +90,21 @@ func (s *service) GetUsersAndMessage(ds connection.DataSender, clientInfo tokent s.storage.AddConnection(clientInfo.UserID.String()) } - response := types.Response{ - Data: req.Data, - } - - marshal, err := json.Marshal(response) - if err != nil { - return nil, nil, err - } - - if req.Server == nil { - s.gameConfigService.Do(req.Token, clientInfo, room.GameConfigs, req.Data) - id := uuid.New() - req.Server = &id - err = s.NotifyAllServers(clientInfo, req) - if err != nil { - s.logger.Warn().Err(err).Msg("NotifyAllServers err") - } - } - - return users, marshal, err + return users, req, err } -func (s *service) RemoveUser(token string, userID uuid.UUID) (err error) { - clientInfo, err := s.token.ParseToken(token) +func (s *service) RemoveUser(clientInfo tokentype.Info, userID uuid.UUID) (err error) { + game, err := s.getRoom(clientInfo) if err != nil { return err } - game, err := s.getRoom(clientInfo) - if err != nil { - return err + for _, user := range game.GetUser() { + if user.ID == userID { + if user.Connection != nil { + user.Connection.Close() + } + } } game.RemoveUser(userID) @@ -211,7 +126,7 @@ func (s *service) SetNewConnection(clientInfo tokentype.Info, ds connection.Data if err != nil { return err } - room.SetUser(&entities.User{ + room.SetUser(&types.User{ ID: clientInfo.UserID, Connection: ds, }) @@ -221,22 +136,7 @@ func (s *service) SetNewConnection(clientInfo tokentype.Info, ds connection.Data return nil } -func (s *service) GetGameResults(token string) (results []types.GameConfigResults, err error) { - clientInfo, err := s.token.ParseToken(token) - if err != nil { - return results, err - } - - playersOnline := s.storage.GetAllConnection() - roomsResults, ok := s.gameConfigService.GetDeletedRoomsResults(clientInfo, playersOnline) - if !ok { - return results, errors.ErrGameResultsNotFound - } - - return roomsResults, nil -} - -func (s *service) getRoom(clientInfo tokentype.Info) (room *entities.Room, err error) { +func (s *service) getRoom(clientInfo tokentype.Info) (room *types.Room, err error) { roomKey := utils.GenerateRoomKey(clientInfo) roomData, ok := s.storage.GetData(roomKey) @@ -244,7 +144,7 @@ func (s *service) getRoom(clientInfo tokentype.Info) (room *entities.Room, err e return room, errors.ErrRoomNotFound } - room, ok = roomData.(*entities.Room) + room, ok = roomData.(*types.Room) if !ok { return room, errors.ErrRoomBadValue } @@ -252,18 +152,17 @@ func (s *service) getRoom(clientInfo tokentype.Info) (room *entities.Room, err e return room, nil } -func (s *service) setRoom(clientInfo tokentype.Info, room *entities.Room) { +func (s *service) setRoom(clientInfo tokentype.Info, room *types.Room) { roomKey := utils.GenerateRoomKey(clientInfo) s.storage.SetData(roomKey, room) } -func NewService(token tokengenerator.TokenGenerator, storage memoryDB.IMemoryDB, gameConfigService configsService.GameConfigsService, logger zerolog.Logger) Service { +func NewService(token tokengenerator.TokenGenerator, storage memoryDB.IMemoryDB, logger zerolog.Logger) Service { srv := &service{ - maxConnections: uint64(types.CountConnectionsMAX()), - storage: storage, - token: token, - gameConfigService: gameConfigService, - logger: logger, + maxConnections: uint64(types.CountConnectionsMAX()), + storage: storage, + token: token, + logger: logger, } go func() { ticker := time.NewTicker(time.Second * 3) diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/storage/memory_db.go b/vendor/github.com/ascenmmo/websocket-server/internal/storage/memory_db.go index ff323f9..0d52a2d 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/storage/memory_db.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/storage/memory_db.go @@ -2,8 +2,6 @@ package memoryDB import ( "context" - "fmt" - "runtime" "sync" "time" ) @@ -40,16 +38,6 @@ type connections struct { count int } -func NewMemoryDb(ctx context.Context, dataTTL time.Duration) *MemoryDb { - db := &MemoryDb{ - userData: &userData{storage: sync.Map{}}, - connections: &connections{storage: sync.Map{}}, - dataTTL: dataTTL, - } - go db.Run(ctx) - return db -} - func (db *MemoryDb) GetData(key string) (any, bool) { value, ok := db.userData.storage.Load(key) if !ok { @@ -128,12 +116,14 @@ func (db *MemoryDb) removeOldData() { return true }) - db.logMemoryUsage() } -func (db *MemoryDb) logMemoryUsage() { - var stats runtime.MemStats - runtime.ReadMemStats(&stats) - fmt.Printf("Memory Usage: Alloc = %v MiB, TotalAlloc = %v MiB, Sys = %v MiB, NumGC = %v\n", - stats.Alloc/1024/1024, stats.TotalAlloc/1024/1024, stats.Sys/1024/1024, stats.NumGC) +func NewMemoryDb(ctx context.Context, dataTTL time.Duration) *MemoryDb { + db := &MemoryDb{ + userData: &userData{storage: sync.Map{}}, + connections: &connections{storage: sync.Map{}}, + dataTTL: dataTTL, + } + go db.Run(ctx) + return db } diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/server_settings.go b/vendor/github.com/ascenmmo/websocket-server/pkg/api/server_settings.go similarity index 70% rename from vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/server_settings.go rename to vendor/github.com/ascenmmo/websocket-server/pkg/api/server_settings.go index 77554b2..bfb0c88 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/server_settings.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/api/server_settings.go @@ -6,12 +6,11 @@ //go:generate tg transport --services . --out ../../pkg/transport --outSwagger ../../pkg/swagger.yaml //go:generate tg client -go --services . --outPath ../../pkg/clients/wsGameServer -package restconnection +package api import ( "context" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/websocket-server/pkg/api/types" ) // @tg http-prefix=api/v1/udp/ @@ -30,10 +29,4 @@ type ServerSettings interface { // @tg http-headers=token|Token // @tg summary=`CreateRoom` CreateRoom(ctx context.Context, token string, createRoom types.CreateRoomRequest) (err error) - // @tg http-headers=token|Token - // @tg summary=`GetGameResults` - GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) - // @tg http-headers=token|Token - // @tg summary=`SetNotifyServer` - SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) } diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/network.go b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/network.go new file mode 100644 index 0000000..8477730 --- /dev/null +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/network.go @@ -0,0 +1,4 @@ +package types + +type CreateRoomRequest struct { +} diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/entities/room.go b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/room.go similarity index 93% rename from vendor/github.com/ascenmmo/websocket-server/pkg/entities/room.go rename to vendor/github.com/ascenmmo/websocket-server/pkg/api/types/room.go index 2dd47d5..0c17196 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/entities/room.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/room.go @@ -1,8 +1,7 @@ -package entities +package types import ( "github.com/ascenmmo/websocket-server/internal/connection" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" "github.com/google/uuid" "sync" "time" @@ -16,8 +15,6 @@ type Room struct { Users []*User - GameConfigs types.GameConfigs - UpdatedAt time.Time mtx sync.RWMutex } diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/settings.go b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/settings.go similarity index 82% rename from vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/settings.go rename to vendor/github.com/ascenmmo/websocket-server/pkg/api/types/settings.go index 9b9e5ac..5ff9286 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/settings.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/settings.go @@ -1,7 +1,6 @@ package types import ( - "fmt" "github.com/ascenmmo/websocket-server/env" "runtime" ) @@ -32,10 +31,6 @@ func CountConnectionsMAX() int { runtime.ReadMemStats(&memStats) connections := calculateConnections(numCPUs, memStats.Sys) - fmt.Printf("Количество CPU: %d\n", numCPUs) - fmt.Printf("Объем оперативной памяти: %d MB\n", memStats.Sys/(1024*1024)) - fmt.Printf("Рекомендуемое количество соединений по UDP: %d\n", connections) - return connections } diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/game_config.go b/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/game_config.go deleted file mode 100644 index beba11e..0000000 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/game_config.go +++ /dev/null @@ -1,45 +0,0 @@ -package types - -import ( - tokentype "github.com/ascenmmo/token-generator/token_type" - "github.com/google/uuid" -) - -type GameConfigExecutor interface { - Name() string - Execute(clientInfo tokentype.Info, config SortingConfig, newUserData interface{}, oldResult GameConfigResults) (newResult GameConfigResults, err error) -} - -type GameConfigResults struct { - GameID uuid.UUID `json:"game_id"` - RoomID uuid.UUID `json:"room_id"` - Result map[string]interface{} `json:"result"` -} - -type GameConfigs struct { - GameID uuid.UUID `json:"game_id" bson:"_id"` - SortingConfig []SortingConfig `json:"sorting_config" bson:"sorting_config"` - IsExists bool `json:"-"` -} - -type SortingConfig struct { - Name string `json:"name" bson:"name"` - Params []ParamMetadata `json:"params" bson:"params"` - UseOnServerType string `json:"use_on_server_type" bson:"use_on_server_type"` - ResultName string `json:"result_name" bson:"result_name"` - ResultType string `json:"result_type" bson:"result_type"` - Executor GameConfigExecutor `json:"-"` -} - -type ParamMetadata struct { - ColumnName string `json:"column_name" bson:"column_name"` - ValueType string `json:"value_type" bson:"value_type"` -} - -func (g *GameConfigs) RemoveSortingConfig(name string) { - for i, v := range g.SortingConfig { - if v.Name == name { - g.SortingConfig = append(g.SortingConfig[:i], g.SortingConfig[i+1:]...) - } - } -} diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/network.go b/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/network.go deleted file mode 100644 index 151539f..0000000 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/restconnection/types/network.go +++ /dev/null @@ -1,19 +0,0 @@ -package types - -import "github.com/google/uuid" - -type Request struct { - Server *uuid.UUID `json:"server,omitempty"` - Token string `json:"token,omitempty"` - Data any `json:"data"` -} - -type Response struct { - Server *uuid.UUID `json:"server,omitempty"` - Data any `json:"data"` -} - -type CreateRoomRequest struct { - GameConfigs GameConfigs `json:"gameConfigs"` - TTL string `json:"time_to_live"` -} diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/start/start.go b/vendor/github.com/ascenmmo/websocket-server/pkg/start/start.go index 72f6eac..c4f274b 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/start/start.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/start/start.go @@ -7,33 +7,34 @@ import ( "github.com/ascenmmo/websocket-server/internal/handler/tcp" "github.com/ascenmmo/websocket-server/internal/handler/ws" "github.com/ascenmmo/websocket-server/internal/service" - configsService "github.com/ascenmmo/websocket-server/internal/service/configs_service" "github.com/ascenmmo/websocket-server/internal/storage" "github.com/ascenmmo/websocket-server/internal/utils" "github.com/ascenmmo/websocket-server/pkg/transport" "github.com/rs/zerolog" + "runtime" "time" ) -func StartWebSocket(ctx context.Context, address, tcpPort, wsPort string, token string, ratelimit int, dataTTL, gameConfigResultsTTl time.Duration, logger zerolog.Logger) (err error) { +func StartWebSocket(ctx context.Context, address, tcpPort, wsPort string, token string, ratelimit int, dataTTL time.Duration, logger zerolog.Logger, logWithMemoryUsage bool) (err error) { ramDB := memoryDB.NewMemoryDb(ctx, dataTTL) - gameConfigResultsDB := memoryDB.NewMemoryDb(ctx, gameConfigResultsTTl) rateLimitDB := memoryDB.NewMemoryDb(ctx, 1) - rateLimitBadMessageDB := memoryDB.NewMemoryDb(ctx, 1) tokenGenerator, err := tokengenerator.NewTokenGenerator(token) if err != nil { return err } - gameConfigService := configsService.NewGameConfigsService(gameConfigResultsDB, tokenGenerator) - newService := service.NewService(tokenGenerator, ramDB, gameConfigService, logger) + newService := service.NewService(tokenGenerator, ramDB, logger) errors := make(chan error) + if logWithMemoryUsage { + logMemoryUsage(logger) + } + go func() { logger.Info().Msg(fmt.Sprintf("ws server listening on %s:%s ", address, wsPort)) - newWS := wsconnection.NewWebSocket(newService, utils.NewRateLimit(ratelimit, rateLimitDB), utils.NewRateLimit(ratelimit, rateLimitBadMessageDB), logger) + newWS := wsconnection.NewWebSocket(newService, utils.NewRateLimit(ratelimit, rateLimitDB), logger) err = newWS.Run(":" + wsPort) if err != nil { errors <- err @@ -59,3 +60,19 @@ func StartWebSocket(ctx context.Context, address, tcpPort, wsPort string, token return err } + +func logMemoryUsage(logger zerolog.Logger) { + ticker := time.NewTicker(time.Second * 10) + go func() { + for range ticker.C { + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + logger.Info(). + Interface("num cpu", runtime.NumCPU()). + Interface("Memory Usage", stats.Alloc/1024/1024). + Interface("TotalAlloc", stats.TotalAlloc/1024/1024). + Interface("Sys", stats.Sys/1024/1024). + Interface("NumGC", stats.NumGC) + } + }() +} diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/jsonrpc.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/jsonrpc.go index 5b8ca93..89f456e 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/jsonrpc.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/jsonrpc.go @@ -150,10 +150,6 @@ func (srv *Server) doSingleBatch(ctx *fiber.Ctx, request baseJsonRPC) (response return srv.httpServerSettings.getServerSettings(ctx, request) case "serversettings.createroom": return srv.httpServerSettings.createRoom(ctx, request) - case "serversettings.getgameresults": - return srv.httpServerSettings.getGameResults(ctx, request) - case "serversettings.setnotifyserver": - return srv.httpServerSettings.setNotifyServer(ctx, request) default: ext.Error.Set(span, true) span.SetTag("msg", "invalid method '"+methodNameOrigin+"'") diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-exchange.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-exchange.go index edd66c8..e7e5550 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-exchange.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-exchange.go @@ -1,10 +1,7 @@ // GENERATED BY 'T'ransport 'G'enerator. DO NOT EDIT. package transport -import ( - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" - "github.com/google/uuid" -) +import "github.com/ascenmmo/websocket-server/pkg/api/types" type requestServerSettingsGetConnectionsNum struct { Token string `json:"token"` @@ -38,20 +35,3 @@ type requestServerSettingsCreateRoom struct { // Formal exchange type, please do not delete. type responseServerSettingsCreateRoom struct{} - -type requestServerSettingsGetGameResults struct { - Token string `json:"token"` -} - -type responseServerSettingsGetGameResults struct { - GameConfigResults []types.GameConfigResults `json:"gameConfigResults"` -} - -type requestServerSettingsSetNotifyServer struct { - Token string `json:"token"` - Id uuid.UUID `json:"id"` - Url string `json:"url"` -} - -// Formal exchange type, please do not delete. -type responseServerSettingsSetNotifyServer struct{} diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-http.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-http.go index 5bf1417..f47028f 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-http.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-http.go @@ -2,7 +2,7 @@ package transport import ( - "github.com/ascenmmo/websocket-server/pkg/restconnection" + "github.com/ascenmmo/websocket-server/pkg/api" "github.com/gofiber/fiber/v2" ) @@ -11,10 +11,10 @@ type httpServerSettings struct { maxBatchSize int maxParallelBatch int svc *serverServerSettings - base restconnection.ServerSettings + base api.ServerSettings } -func NewServerSettings(svcServerSettings restconnection.ServerSettings) (srv *httpServerSettings) { +func NewServerSettings(svcServerSettings api.ServerSettings) (srv *httpServerSettings) { srv = &httpServerSettings{ base: svcServerSettings, @@ -48,6 +48,4 @@ func (http *httpServerSettings) SetRoutes(route *fiber.App) { route.Post("/api/v1/udp/serverSettings/healthCheck", http.serveHealthCheck) route.Post("/api/v1/udp/serverSettings/getServerSettings", http.serveGetServerSettings) route.Post("/api/v1/udp/serverSettings/createRoom", http.serveCreateRoom) - route.Post("/api/v1/udp/serverSettings/getGameResults", http.serveGetGameResults) - route.Post("/api/v1/udp/serverSettings/setNotifyServer", http.serveSetNotifyServer) } diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-jsonrpc.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-jsonrpc.go index ae45b35..22af17e 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-jsonrpc.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-jsonrpc.go @@ -238,120 +238,6 @@ func (http *httpServerSettings) createRoom(ctx *fiber.Ctx, requestBase baseJsonR } return } -func (http *httpServerSettings) serveGetGameResults(ctx *fiber.Ctx) (err error) { - return http.serveMethod(ctx, "getgameresults", http.getGameResults) -} -func (http *httpServerSettings) getGameResults(ctx *fiber.Ctx, requestBase baseJsonRPC) (responseBase *baseJsonRPC) { - - var err error - var request requestServerSettingsGetGameResults - - methodCtx := ctx.UserContext() - span := otg.SpanFromContext(methodCtx) - span.SetTag("method", "getGameResults") - - if requestBase.Params != nil { - if err = json.Unmarshal(requestBase.Params, &request); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "request body could not be decoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "request body could not be decoded: "+err.Error(), nil) - } - } - if requestBase.Version != Version { - ext.Error.Set(span, true) - span.SetTag("msg", "incorrect protocol version: "+requestBase.Version) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "incorrect protocol version: "+requestBase.Version, nil) - } - - if _token := string(ctx.Request().Header.Peek("Token")); _token != "" { - var token string - token = _token - request.Token = token - } - - var response responseServerSettingsGetGameResults - response.GameConfigResults, err = http.svc.GetGameResults(methodCtx, request.Token) - if err != nil { - if http.errorHandler != nil { - err = http.errorHandler(err) - } - ext.Error.Set(span, true) - span.SetTag("msg", err) - span.SetTag("errData", toString(err)) - code := internalError - if errCoder, ok := err.(withErrorCode); ok { - code = errCoder.Code() - } - return makeErrorResponseJsonRPC(requestBase.ID, code, err.Error(), err) - } - responseBase = &baseJsonRPC{ - ID: requestBase.ID, - Version: Version, - } - if responseBase.Result, err = json.Marshal(response); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "response body could not be encoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "response body could not be encoded: "+err.Error(), nil) - } - return -} -func (http *httpServerSettings) serveSetNotifyServer(ctx *fiber.Ctx) (err error) { - return http.serveMethod(ctx, "setnotifyserver", http.setNotifyServer) -} -func (http *httpServerSettings) setNotifyServer(ctx *fiber.Ctx, requestBase baseJsonRPC) (responseBase *baseJsonRPC) { - - var err error - var request requestServerSettingsSetNotifyServer - - methodCtx := ctx.UserContext() - span := otg.SpanFromContext(methodCtx) - span.SetTag("method", "setNotifyServer") - - if requestBase.Params != nil { - if err = json.Unmarshal(requestBase.Params, &request); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "request body could not be decoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "request body could not be decoded: "+err.Error(), nil) - } - } - if requestBase.Version != Version { - ext.Error.Set(span, true) - span.SetTag("msg", "incorrect protocol version: "+requestBase.Version) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "incorrect protocol version: "+requestBase.Version, nil) - } - - if _token := string(ctx.Request().Header.Peek("Token")); _token != "" { - var token string - token = _token - request.Token = token - } - - var response responseServerSettingsSetNotifyServer - err = http.svc.SetNotifyServer(methodCtx, request.Token, request.Id, request.Url) - if err != nil { - if http.errorHandler != nil { - err = http.errorHandler(err) - } - ext.Error.Set(span, true) - span.SetTag("msg", err) - span.SetTag("errData", toString(err)) - code := internalError - if errCoder, ok := err.(withErrorCode); ok { - code = errCoder.Code() - } - return makeErrorResponseJsonRPC(requestBase.ID, code, err.Error(), err) - } - responseBase = &baseJsonRPC{ - ID: requestBase.ID, - Version: Version, - } - if responseBase.Result, err = json.Marshal(response); err != nil { - ext.Error.Set(span, true) - span.SetTag("msg", "response body could not be encoded: "+err.Error()) - return makeErrorResponseJsonRPC(requestBase.ID, parseError, "response body could not be encoded: "+err.Error(), nil) - } - return -} func (http *httpServerSettings) serveMethod(ctx *fiber.Ctx, methodName string, methodHandler methodJsonRPC) (err error) { span := otg.SpanFromContext(ctx.UserContext()) @@ -475,10 +361,6 @@ func (http *httpServerSettings) doSingleBatch(ctx *fiber.Ctx, request baseJsonRP return http.getServerSettings(ctx, request) case "createroom": return http.createRoom(ctx, request) - case "getgameresults": - return http.getGameResults(ctx, request) - case "setnotifyserver": - return http.setNotifyServer(ctx, request) default: ext.Error.Set(span, true) span.SetTag("msg", "invalid method '"+methodNameOrigin+"'") diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-logger.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-logger.go index b09033f..ee23abc 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-logger.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-logger.go @@ -3,21 +3,20 @@ package transport import ( "context" - "github.com/ascenmmo/websocket-server/pkg/restconnection" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" + "github.com/ascenmmo/websocket-server/pkg/api" + "github.com/ascenmmo/websocket-server/pkg/api/types" "github.com/ascenmmo/websocket-server/pkg/transport/viewer" - "github.com/google/uuid" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "time" ) type loggerServerSettings struct { - next restconnection.ServerSettings + next api.ServerSettings } func loggerMiddlewareServerSettings() MiddlewareServerSettings { - return func(next restconnection.ServerSettings) restconnection.ServerSettings { + return func(next api.ServerSettings) api.ServerSettings { return &loggerServerSettings{next: next} } } @@ -103,45 +102,3 @@ func (m loggerServerSettings) CreateRoom(ctx context.Context, token string, crea }(time.Now()) return m.next.CreateRoom(ctx, token, createRoom) } - -func (m loggerServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - logger := log.Ctx(ctx).With().Str("service", "ServerSettings").Str("method", "getGameResults").Logger() - defer func(begin time.Time) { - logHandle := func(ev *zerolog.Event) { - fields := map[string]interface{}{ - "request": viewer.Sprintf("%+v", requestServerSettingsGetGameResults{Token: token}), - "response": viewer.Sprintf("%+v", responseServerSettingsGetGameResults{GameConfigResults: gameConfigResults}), - } - ev.Fields(fields).Str("took", time.Since(begin).String()) - } - if err != nil { - logger.Error().Err(err).Func(logHandle).Msg("call getGameResults") - return - } - logger.Info().Func(logHandle).Msg("call getGameResults") - }(time.Now()) - return m.next.GetGameResults(ctx, token) -} - -func (m loggerServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - logger := log.Ctx(ctx).With().Str("service", "ServerSettings").Str("method", "setNotifyServer").Logger() - defer func(begin time.Time) { - logHandle := func(ev *zerolog.Event) { - fields := map[string]interface{}{ - "request": viewer.Sprintf("%+v", requestServerSettingsSetNotifyServer{ - Id: id, - Token: token, - Url: url, - }), - "response": viewer.Sprintf("%+v", responseServerSettingsSetNotifyServer{}), - } - ev.Fields(fields).Str("took", time.Since(begin).String()) - } - if err != nil { - logger.Error().Err(err).Func(logHandle).Msg("call setNotifyServer") - return - } - logger.Info().Func(logHandle).Msg("call setNotifyServer") - }(time.Now()) - return m.next.SetNotifyServer(ctx, token, id, url) -} diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-middleware.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-middleware.go index aa27ed5..02f905b 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-middleware.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-middleware.go @@ -3,23 +3,18 @@ package transport import ( "context" - "github.com/ascenmmo/websocket-server/pkg/restconnection" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/websocket-server/pkg/api" + "github.com/ascenmmo/websocket-server/pkg/api/types" ) type ServerSettingsGetConnectionsNum func(ctx context.Context, token string) (countConn int, exists bool, err error) type ServerSettingsHealthCheck func(ctx context.Context, token string) (exists bool, err error) type ServerSettingsGetServerSettings func(ctx context.Context, token string) (settings types.Settings, err error) type ServerSettingsCreateRoom func(ctx context.Context, token string, createRoom types.CreateRoomRequest) (err error) -type ServerSettingsGetGameResults func(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) -type ServerSettingsSetNotifyServer func(ctx context.Context, token string, id uuid.UUID, url string) (err error) -type MiddlewareServerSettings func(next restconnection.ServerSettings) restconnection.ServerSettings +type MiddlewareServerSettings func(next api.ServerSettings) api.ServerSettings type MiddlewareServerSettingsGetConnectionsNum func(next ServerSettingsGetConnectionsNum) ServerSettingsGetConnectionsNum type MiddlewareServerSettingsHealthCheck func(next ServerSettingsHealthCheck) ServerSettingsHealthCheck type MiddlewareServerSettingsGetServerSettings func(next ServerSettingsGetServerSettings) ServerSettingsGetServerSettings type MiddlewareServerSettingsCreateRoom func(next ServerSettingsCreateRoom) ServerSettingsCreateRoom -type MiddlewareServerSettingsGetGameResults func(next ServerSettingsGetGameResults) ServerSettingsGetGameResults -type MiddlewareServerSettingsSetNotifyServer func(next ServerSettingsSetNotifyServer) ServerSettingsSetNotifyServer diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-server.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-server.go index fb297e6..6a3123d 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-server.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-server.go @@ -3,19 +3,16 @@ package transport import ( "context" - "github.com/ascenmmo/websocket-server/pkg/restconnection" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/websocket-server/pkg/api" + "github.com/ascenmmo/websocket-server/pkg/api/types" ) type serverServerSettings struct { - svc restconnection.ServerSettings + svc api.ServerSettings getConnectionsNum ServerSettingsGetConnectionsNum healthCheck ServerSettingsHealthCheck getServerSettings ServerSettingsGetServerSettings createRoom ServerSettingsCreateRoom - getGameResults ServerSettingsGetGameResults - setNotifyServer ServerSettingsSetNotifyServer } type MiddlewareSetServerSettings interface { @@ -24,21 +21,17 @@ type MiddlewareSetServerSettings interface { WrapHealthCheck(m MiddlewareServerSettingsHealthCheck) WrapGetServerSettings(m MiddlewareServerSettingsGetServerSettings) WrapCreateRoom(m MiddlewareServerSettingsCreateRoom) - WrapGetGameResults(m MiddlewareServerSettingsGetGameResults) - WrapSetNotifyServer(m MiddlewareServerSettingsSetNotifyServer) WithTrace() WithLog() } -func newServerServerSettings(svc restconnection.ServerSettings) *serverServerSettings { +func newServerServerSettings(svc api.ServerSettings) *serverServerSettings { return &serverServerSettings{ createRoom: svc.CreateRoom, getConnectionsNum: svc.GetConnectionsNum, - getGameResults: svc.GetGameResults, getServerSettings: svc.GetServerSettings, healthCheck: svc.HealthCheck, - setNotifyServer: svc.SetNotifyServer, svc: svc, } } @@ -49,8 +42,6 @@ func (srv *serverServerSettings) Wrap(m MiddlewareServerSettings) { srv.healthCheck = srv.svc.HealthCheck srv.getServerSettings = srv.svc.GetServerSettings srv.createRoom = srv.svc.CreateRoom - srv.getGameResults = srv.svc.GetGameResults - srv.setNotifyServer = srv.svc.SetNotifyServer } func (srv *serverServerSettings) GetConnectionsNum(ctx context.Context, token string) (countConn int, exists bool, err error) { @@ -69,14 +60,6 @@ func (srv *serverServerSettings) CreateRoom(ctx context.Context, token string, c return srv.createRoom(ctx, token, createRoom) } -func (srv *serverServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - return srv.getGameResults(ctx, token) -} - -func (srv *serverServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - return srv.setNotifyServer(ctx, token, id, url) -} - func (srv *serverServerSettings) WrapGetConnectionsNum(m MiddlewareServerSettingsGetConnectionsNum) { srv.getConnectionsNum = m(srv.getConnectionsNum) } @@ -93,14 +76,6 @@ func (srv *serverServerSettings) WrapCreateRoom(m MiddlewareServerSettingsCreate srv.createRoom = m(srv.createRoom) } -func (srv *serverServerSettings) WrapGetGameResults(m MiddlewareServerSettingsGetGameResults) { - srv.getGameResults = m(srv.getGameResults) -} - -func (srv *serverServerSettings) WrapSetNotifyServer(m MiddlewareServerSettingsSetNotifyServer) { - srv.setNotifyServer = m(srv.setNotifyServer) -} - func (srv *serverServerSettings) WithTrace() { srv.Wrap(traceMiddlewareServerSettings) } diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-trace.go b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-trace.go index 889571c..ada7d04 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-trace.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/transport/serversettings-trace.go @@ -3,17 +3,16 @@ package transport import ( "context" - "github.com/ascenmmo/websocket-server/pkg/restconnection" - "github.com/ascenmmo/websocket-server/pkg/restconnection/types" - "github.com/google/uuid" + "github.com/ascenmmo/websocket-server/pkg/api" + "github.com/ascenmmo/websocket-server/pkg/api/types" "github.com/opentracing/opentracing-go" ) type traceServerSettings struct { - next restconnection.ServerSettings + next api.ServerSettings } -func traceMiddlewareServerSettings(next restconnection.ServerSettings) restconnection.ServerSettings { +func traceMiddlewareServerSettings(next api.ServerSettings) api.ServerSettings { return &traceServerSettings{next: next} } @@ -40,15 +39,3 @@ func (svc traceServerSettings) CreateRoom(ctx context.Context, token string, cre span.SetTag("method", "CreateRoom") return svc.next.CreateRoom(ctx, token, createRoom) } - -func (svc traceServerSettings) GetGameResults(ctx context.Context, token string) (gameConfigResults []types.GameConfigResults, err error) { - span := opentracing.SpanFromContext(ctx) - span.SetTag("method", "GetGameResults") - return svc.next.GetGameResults(ctx, token) -} - -func (svc traceServerSettings) SetNotifyServer(ctx context.Context, token string, id uuid.UUID, url string) (err error) { - span := opentracing.SpanFromContext(ctx) - span.SetTag("method", "SetNotifyServer") - return svc.next.SetNotifyServer(ctx, token, id, url) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 98b8c24..79fe1e2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,22 +1,16 @@ # github.com/andybalholm/brotli v1.0.5 ## explicit; go 1.12 github.com/andybalholm/brotli -# github.com/ascenmmo/tcp-server v0.0.0-20241024213218-104f0955af07 +# github.com/ascenmmo/tcp-server v1.0.1 ## explicit; go 1.23.2 github.com/ascenmmo/tcp-server/env -github.com/ascenmmo/tcp-server/internal/entities github.com/ascenmmo/tcp-server/internal/handler github.com/ascenmmo/tcp-server/internal/service -github.com/ascenmmo/tcp-server/internal/service/configs_service github.com/ascenmmo/tcp-server/internal/storage github.com/ascenmmo/tcp-server/internal/utils -github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer -github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/cb -github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/hasher -github.com/ascenmmo/tcp-server/pkg/clients/tcpGameServer/jsonrpc +github.com/ascenmmo/tcp-server/pkg/api +github.com/ascenmmo/tcp-server/pkg/api/types github.com/ascenmmo/tcp-server/pkg/errors -github.com/ascenmmo/tcp-server/pkg/restconnection -github.com/ascenmmo/tcp-server/pkg/restconnection/types github.com/ascenmmo/tcp-server/pkg/start github.com/ascenmmo/tcp-server/pkg/transport github.com/ascenmmo/tcp-server/pkg/transport/viewer @@ -24,41 +18,37 @@ github.com/ascenmmo/tcp-server/pkg/transport/viewer ## explicit; go 1.23.2 github.com/ascenmmo/token-generator/token_generator github.com/ascenmmo/token-generator/token_type -# github.com/ascenmmo/udp-server v0.0.0-20241104222117-5f30f0f99117 +# github.com/ascenmmo/udp-server v1.0.1 ## explicit; go 1.23.2 github.com/ascenmmo/udp-server/env github.com/ascenmmo/udp-server/internal/connection -github.com/ascenmmo/udp-server/internal/entities github.com/ascenmmo/udp-server/internal/handler/tcp github.com/ascenmmo/udp-server/internal/handler/udp github.com/ascenmmo/udp-server/internal/service -github.com/ascenmmo/udp-server/internal/service/configs_service github.com/ascenmmo/udp-server/internal/storage github.com/ascenmmo/udp-server/internal/utils +github.com/ascenmmo/udp-server/pkg/api +github.com/ascenmmo/udp-server/pkg/api/types github.com/ascenmmo/udp-server/pkg/clients/udpGameServer github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/cb github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/hasher github.com/ascenmmo/udp-server/pkg/clients/udpGameServer/jsonrpc github.com/ascenmmo/udp-server/pkg/errors -github.com/ascenmmo/udp-server/pkg/restconnection -github.com/ascenmmo/udp-server/pkg/restconnection/types github.com/ascenmmo/udp-server/pkg/start github.com/ascenmmo/udp-server/pkg/transport github.com/ascenmmo/udp-server/pkg/transport/viewer -# github.com/ascenmmo/websocket-server v0.0.0-20241024213056-3c1271cc0529 +# github.com/ascenmmo/websocket-server v1.0.1 ## explicit; go 1.23.2 github.com/ascenmmo/websocket-server/env github.com/ascenmmo/websocket-server/internal/connection github.com/ascenmmo/websocket-server/internal/handler/tcp github.com/ascenmmo/websocket-server/internal/handler/ws github.com/ascenmmo/websocket-server/internal/service -github.com/ascenmmo/websocket-server/internal/service/configs_service github.com/ascenmmo/websocket-server/internal/storage github.com/ascenmmo/websocket-server/internal/utils -github.com/ascenmmo/websocket-server/pkg/entities +github.com/ascenmmo/websocket-server/pkg/api +github.com/ascenmmo/websocket-server/pkg/api/types github.com/ascenmmo/websocket-server/pkg/errors -github.com/ascenmmo/websocket-server/pkg/restconnection -github.com/ascenmmo/websocket-server/pkg/restconnection/types github.com/ascenmmo/websocket-server/pkg/start github.com/ascenmmo/websocket-server/pkg/transport github.com/ascenmmo/websocket-server/pkg/transport/viewer From 64529ee4859782ec76e4a9315612c569e45ebcd7 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Fri, 8 Nov 2024 02:07:11 +0300 Subject: [PATCH 03/21] fix some errors --- Dockerfile | 2 ++ README.md | 2 +- RU_README.md | 2 +- env/env.go | 4 ++-- internal/service/dev_tools/connections.go | 2 +- pkg/multiplayer/types/server.go | 9 +-------- 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index fe37db2..892385f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ WORKDIR $GOPATH/src ADD . . ENV GO111MODULE=on +RUN go mod vendor + RUN go build -o /bin/app ./cmd/multiplayer FROM ubuntu:24.04 diff --git a/README.md b/README.md index e87395d..de86654 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ var ( RunMultiplayer = true // Enable multiplayer mode RunAdminPanel = true // Enable admin panel MultiplayerPort = "8080" // Port for multiplayer mode - MongoURL = "mongodb://username:userpassword@ascenmmo.com:27017/?maxPoolSize=20&w=majority" // MongoDB connection URL + MongoURL = "mongodb://username:userpassword@ascenmmo.com:27017" // MongoDB connection URL MultiplayerMaxRequestPerSecond = 5 // Maximum requests per second for multiplayer mode ) diff --git a/RU_README.md b/RU_README.md index 25d1a73..ff328c8 100644 --- a/RU_README.md +++ b/RU_README.md @@ -39,7 +39,7 @@ var ( RunMultiplayer = true // Запуск многопользовательского режима RunAdminPanel = true // Запуск админки MultiplayerPort = "8080" // Порт для многопользовательского режима - MongoURL = "mongodb://username:userpassword@ascenmmo.com:27017/?maxPoolSize=20&w=majority" // URL подключения к MongoDB + MongoURL = "mongodb://username:userpassword@ascenmmo.com:27017" // URL подключения к MongoDB MultiplayerMaxRequestPerSecond = 5 // Максимальное количество запросов в секунду для многопользовательского режима ) diff --git a/env/env.go b/env/env.go index 36af062..a18f036 100644 --- a/env/env.go +++ b/env/env.go @@ -2,14 +2,14 @@ package env var ( ServerAddress = "ascenmmo.com" - TokenKey = "_remember_token_mast_be_32_bytes" + TokenKey = "_remember_token_must_be_32_bytes" ) var ( RunMultiplayer = true RunAdminPanel = true MultiplayerPort = "8080" - MongoURL = "mongodb://username:userpassword@ascenmmo.com:27017/?maxPoolSize=20&w=majority" + MongoURL = "mongodb://username:userpassword@ascenmmo.com:27017" MultiplayerMaxRequestPerSecond = 5 ) diff --git a/internal/service/dev_tools/connections.go b/internal/service/dev_tools/connections.go index a91f4f1..4e6681c 100644 --- a/internal/service/dev_tools/connections.go +++ b/internal/service/dev_tools/connections.go @@ -51,7 +51,7 @@ func (c *connections) CreateRoom(ctx context.Context, token string, gameID uuid. for i := range servers { exists, err := servers[i].IsExists(ctx, token) if err != nil { - c.logger.Error().Err(err) + c.logger.Error().Err(err).Msg("IsExists error") continue } if exists { diff --git a/pkg/multiplayer/types/server.go b/pkg/multiplayer/types/server.go index 638d28e..ef9bc8e 100644 --- a/pkg/multiplayer/types/server.go +++ b/pkg/multiplayer/types/server.go @@ -40,12 +40,6 @@ type ConnectionServer struct { func (s *Server) IsExists(ctx context.Context, token string) (bool, error) { var err error cli := udpGameServer.New(s.getRestUrl()) - exists, err := cli.ServerSettings().HealthCheck(context.TODO(), token) - if err != nil { - s.IsActive = false - return false, err - } - settings, err := cli.ServerSettings().GetServerSettings(ctx, token) if err != nil { s.IsActive = false @@ -56,8 +50,7 @@ func (s *Server) IsExists(ctx context.Context, token string) (bool, error) { s.ConnectionPort = settings.ConnectionPort s.IsActive = true - s.IsActive = exists - return exists, err + return s.IsActive, err } func (s *Server) CreateRoom(ctx context.Context, token string) (err error) { From 90641d99c9cef624a4ba7993afe7a0732ec34aa1 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sat, 9 Nov 2024 01:51:56 +0300 Subject: [PATCH 04/21] fix some files --- cmd/multiplayer/main.go | 11 ++++++++++ docker-compose.yml | 1 + env/env.go | 2 +- internal/start/multiplatform.go | 20 +++++++++++++++++++ .../assets/pages/game_info/index.html | 2 +- 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cmd/multiplayer/main.go b/cmd/multiplayer/main.go index 9c74e7c..0adeef2 100644 --- a/cmd/multiplayer/main.go +++ b/cmd/multiplayer/main.go @@ -5,6 +5,9 @@ import ( "github.com/ascenmmo/multiplayer-game-servers/env" "github.com/ascenmmo/multiplayer-game-servers/internal/start" "github.com/rs/zerolog" + "log" + "net/http" + _ "net/http/pprof" "os" "os/signal" "syscall" @@ -33,5 +36,13 @@ func main() { go start.TcpServer(ctx, logger) } + prof() + <-shutdown } + +func prof() { + go func() { + log.Println(http.ListenAndServe(":6060", nil)) + }() +} diff --git a/docker-compose.yml b/docker-compose.yml index 4be3ebe..80baf6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: - "8083:8083" # TCP Server - "4500:4500/udp" # UDP Connection Port - "4240:4240" # WebSocket Connection Port + - "6060:6060" # WebSocket Connection Port mongodb: image: mongo:8.0 diff --git a/env/env.go b/env/env.go index a18f036..996c73d 100644 --- a/env/env.go +++ b/env/env.go @@ -2,7 +2,7 @@ package env var ( ServerAddress = "ascenmmo.com" - TokenKey = "_remember_token_must_be_32_bytes" + TokenKey = "_remember_token_mast_be_32_bytes" ) var ( diff --git a/internal/start/multiplatform.go b/internal/start/multiplatform.go index ca5a5fe..5eaf7eb 100644 --- a/internal/start/multiplatform.go +++ b/internal/start/multiplatform.go @@ -14,6 +14,8 @@ import ( "github.com/gofiber/fiber/v2" "github.com/rs/zerolog" "html/template" + "runtime" + "time" ) func Multiplayer(logger zerolog.Logger) { @@ -59,6 +61,8 @@ func Multiplayer(logger zerolog.Logger) { transport.DevToolsGameConfigs(transport.NewDevToolsGameConfigs(devToolsGameConfigs)), } + logMemoryUsage(logger) + srv := transport.New(logger, services...).WithLog() if env.RunAdminPanel { @@ -72,6 +76,22 @@ func Multiplayer(logger zerolog.Logger) { } } +func logMemoryUsage(logger zerolog.Logger) { + ticker := time.NewTicker(time.Second * 10) + go func() { + for range ticker.C { + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + logger.Info(). + Interface("num cpu", runtime.NumCPU()). + Interface("Memory Usage", stats.Alloc/1024/1024). + Interface("TotalAlloc", stats.TotalAlloc/1024/1024). + Interface("Sys", stats.Sys/1024/1024). + Interface("NumGC", stats.NumGC) + } + }() +} + func mastNil(err error) { if err != nil { panic(err) diff --git a/pkg/admin_client/assets/pages/game_info/index.html b/pkg/admin_client/assets/pages/game_info/index.html index 29825be..5a0e32d 100644 --- a/pkg/admin_client/assets/pages/game_info/index.html +++ b/pkg/admin_client/assets/pages/game_info/index.html @@ -332,7 +332,7 @@

${game.name}

gameID: "${game.gameID}"
- + `; From 0582417e375800467f566a33b82bdad61029b3b9 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 00:41:53 +0300 Subject: [PATCH 05/21] new ws --- go.mod | 2 +- go.sum | 4 ++-- .../websocket-server/internal/service/service.go | 10 ---------- .../ascenmmo/websocket-server/pkg/api/types/room.go | 12 ------------ vendor/modules.txt | 2 +- 5 files changed, 4 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index da00956..70a0f9c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/ascenmmo/tcp-server v1.0.1 github.com/ascenmmo/token-generator v1.0.0 github.com/ascenmmo/udp-server v1.0.1 - github.com/ascenmmo/websocket-server v1.0.1 + github.com/ascenmmo/websocket-server v1.0.2 github.com/go-kit/kit v0.13.0 github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.52.5 diff --git a/go.sum b/go.sum index d7ac0d7..507892a 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/ascenmmo/token-generator v1.0.0 h1:+yUCIXfeO68tuWvYyStT4dnnYWfujfP4vP github.com/ascenmmo/token-generator v1.0.0/go.mod h1:M3RdQKl1JfQeqM2hWNs0645G48GupB7idwdzQMkxlxM= github.com/ascenmmo/udp-server v1.0.1 h1:bOKY/+IJt9wesYYUh5GEmWwgSXpffN+eSKBqdL+/Hc4= github.com/ascenmmo/udp-server v1.0.1/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= -github.com/ascenmmo/websocket-server v1.0.1 h1:MKwqJ6gtS++cjJ9RNaMWweGmDKw1ux1LiCUjjBQ2PVQ= -github.com/ascenmmo/websocket-server v1.0.1/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= +github.com/ascenmmo/websocket-server v1.0.2 h1:JmoRuM+k+sUM5aQ4UjiwwLIX+lDtWG0P173bf16HMNA= +github.com/ascenmmo/websocket-server v1.0.2/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go b/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go index a7e5581..7097d66 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/service/service.go @@ -1,7 +1,6 @@ package service import ( - "fmt" tokengenerator "github.com/ascenmmo/token-generator/token_generator" tokentype "github.com/ascenmmo/token-generator/token_type" "github.com/ascenmmo/websocket-server/internal/connection" @@ -11,8 +10,6 @@ import ( "github.com/ascenmmo/websocket-server/pkg/errors" "github.com/google/uuid" "github.com/rs/zerolog" - "runtime" - "time" ) type Service interface { @@ -164,12 +161,5 @@ func NewService(token tokengenerator.TokenGenerator, storage memoryDB.IMemoryDB, token: token, logger: logger, } - go func() { - ticker := time.NewTicker(time.Second * 3) - for range ticker.C { - fmt.Println(fmt.Sprintf("count connections: %d \t max conections: %d", srv.storage.CountConnection(), srv.maxConnections)) - fmt.Println(fmt.Sprintf("count gorutines: %d ", runtime.NumGoroutine())) - } - }() return srv } diff --git a/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/room.go b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/room.go index 0c17196..27ded01 100644 --- a/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/room.go +++ b/vendor/github.com/ascenmmo/websocket-server/pkg/api/types/room.go @@ -3,7 +3,6 @@ package types import ( "github.com/ascenmmo/websocket-server/internal/connection" "github.com/google/uuid" - "sync" "time" ) @@ -16,7 +15,6 @@ type Room struct { Users []*User UpdatedAt time.Time - mtx sync.RWMutex } type User struct { @@ -26,22 +24,16 @@ type User struct { } func (r *Room) SetUser(user *User) { - r.mtx.Lock() r.Users = r.setUser(r.Users, user) - r.mtx.Unlock() } func (r *Room) GetUser() (users []*User) { - r.mtx.RLock() users = r.Users - r.mtx.RUnlock() return } func (r *Room) RemoveUser(user uuid.UUID) { - r.mtx.Lock() r.removeFromArray(user) - r.mtx.Unlock() } func (r *Room) SetUpdatedAt() { @@ -63,13 +55,11 @@ func (r *Room) setUser(users []*User, user *User) (allUsers []*User) { } func (r *Room) removeFromArray(userID uuid.UUID) { - r.mtx.Lock() for i, user := range r.Users { if user.ID == userID { r.Users = append(r.Users[:i], r.Users[i+1:]...) } } - r.mtx.Unlock() } func (r *Room) SetServerID(id uuid.UUID) { @@ -82,11 +72,9 @@ func (r *Room) SetServerID(id uuid.UUID) { } func (r *Room) RemoveServerID(id uuid.UUID) { - r.mtx.Lock() for i, server := range r.ServerID { if server == id { r.ServerID = append(r.ServerID[:i], r.ServerID[i+1:]...) } } - r.mtx.Unlock() } diff --git a/vendor/modules.txt b/vendor/modules.txt index 79fe1e2..cec4843 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -37,7 +37,7 @@ github.com/ascenmmo/udp-server/pkg/errors github.com/ascenmmo/udp-server/pkg/start github.com/ascenmmo/udp-server/pkg/transport github.com/ascenmmo/udp-server/pkg/transport/viewer -# github.com/ascenmmo/websocket-server v1.0.1 +# github.com/ascenmmo/websocket-server v1.0.2 ## explicit; go 1.23.2 github.com/ascenmmo/websocket-server/env github.com/ascenmmo/websocket-server/internal/connection From d7e782846fb3cc34be74c875c653725cbac0ffcc Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 01:06:43 +0300 Subject: [PATCH 06/21] fix ws --- go.mod | 2 +- go.sum | 4 ++-- vendor/modules.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 70a0f9c..0bbc015 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/ascenmmo/tcp-server v1.0.1 github.com/ascenmmo/token-generator v1.0.0 github.com/ascenmmo/udp-server v1.0.1 - github.com/ascenmmo/websocket-server v1.0.2 + github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0 github.com/go-kit/kit v0.13.0 github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.52.5 diff --git a/go.sum b/go.sum index 507892a..c5c8d7c 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/ascenmmo/token-generator v1.0.0 h1:+yUCIXfeO68tuWvYyStT4dnnYWfujfP4vP github.com/ascenmmo/token-generator v1.0.0/go.mod h1:M3RdQKl1JfQeqM2hWNs0645G48GupB7idwdzQMkxlxM= github.com/ascenmmo/udp-server v1.0.1 h1:bOKY/+IJt9wesYYUh5GEmWwgSXpffN+eSKBqdL+/Hc4= github.com/ascenmmo/udp-server v1.0.1/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= -github.com/ascenmmo/websocket-server v1.0.2 h1:JmoRuM+k+sUM5aQ4UjiwwLIX+lDtWG0P173bf16HMNA= -github.com/ascenmmo/websocket-server v1.0.2/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= +github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0 h1:YzLs5gPhBdKo5mPqIyO4Gfnn56KUksY5EQjlxb6HeQE= +github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/vendor/modules.txt b/vendor/modules.txt index cec4843..8c40731 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -37,7 +37,7 @@ github.com/ascenmmo/udp-server/pkg/errors github.com/ascenmmo/udp-server/pkg/start github.com/ascenmmo/udp-server/pkg/transport github.com/ascenmmo/udp-server/pkg/transport/viewer -# github.com/ascenmmo/websocket-server v1.0.2 +# github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0 ## explicit; go 1.23.2 github.com/ascenmmo/websocket-server/env github.com/ascenmmo/websocket-server/internal/connection From 06014be7bba5b83641edcc10bbc29426e74f5052 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 01:15:49 +0300 Subject: [PATCH 07/21] fix ws panic --- go.mod | 2 +- go.sum | 4 ++-- .../internal/connection/ws.go | 23 ++++++++++++------- vendor/modules.txt | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 0bbc015..ce30c11 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/ascenmmo/tcp-server v1.0.1 github.com/ascenmmo/token-generator v1.0.0 github.com/ascenmmo/udp-server v1.0.1 - github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0 + github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f github.com/go-kit/kit v0.13.0 github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.52.5 diff --git a/go.sum b/go.sum index c5c8d7c..5dbe969 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/ascenmmo/token-generator v1.0.0 h1:+yUCIXfeO68tuWvYyStT4dnnYWfujfP4vP github.com/ascenmmo/token-generator v1.0.0/go.mod h1:M3RdQKl1JfQeqM2hWNs0645G48GupB7idwdzQMkxlxM= github.com/ascenmmo/udp-server v1.0.1 h1:bOKY/+IJt9wesYYUh5GEmWwgSXpffN+eSKBqdL+/Hc4= github.com/ascenmmo/udp-server v1.0.1/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= -github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0 h1:YzLs5gPhBdKo5mPqIyO4Gfnn56KUksY5EQjlxb6HeQE= -github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= +github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f h1:UaAIZI0w9igJpmSUCQDyGo5FZmJJel0F1Yuo0iK3cF8= +github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go b/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go index 0ae3800..d9b5a4a 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go @@ -3,25 +3,32 @@ package connection import ( "context" "github.com/gorilla/websocket" + "sync" ) type WebSocketConnection struct { Conn *websocket.Conn CtxClose context.CancelFunc + mutex sync.Mutex } -func (u *WebSocketConnection) GetID() string { - return u.Conn.RemoteAddr().String() +func (ws *WebSocketConnection) GetID() string { + ws.mutex.Lock() + defer ws.mutex.Unlock() + return ws.Conn.RemoteAddr().String() } -func (u *WebSocketConnection) Write(msg []byte) error { - err := u.Conn.WriteMessage(websocket.BinaryMessage, msg) - +func (ws *WebSocketConnection) Write(msg []byte) error { + ws.mutex.Lock() + defer ws.mutex.Unlock() + err := ws.Conn.WriteMessage(websocket.BinaryMessage, msg) return err } -func (u *WebSocketConnection) Close() { - if u.CtxClose != nil { - u.CtxClose() +func (ws *WebSocketConnection) Close() { + ws.mutex.Lock() + defer ws.mutex.Unlock() + if ws.CtxClose != nil { + ws.CtxClose() } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 8c40731..63057f3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -37,7 +37,7 @@ github.com/ascenmmo/udp-server/pkg/errors github.com/ascenmmo/udp-server/pkg/start github.com/ascenmmo/udp-server/pkg/transport github.com/ascenmmo/udp-server/pkg/transport/viewer -# github.com/ascenmmo/websocket-server v0.0.0-20241109212125-b4a700ccb3f0 +# github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f ## explicit; go 1.23.2 github.com/ascenmmo/websocket-server/env github.com/ascenmmo/websocket-server/internal/connection From 1933b59135e39204e77d6ebb9102a215b841ad30 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 01:32:13 +0300 Subject: [PATCH 08/21] update ws --- go.mod | 2 +- go.sum | 4 ++-- .../websocket-server/internal/connection/dataSender.go | 2 +- .../ascenmmo/websocket-server/internal/connection/ws.go | 4 ++-- .../ascenmmo/websocket-server/internal/handler/ws/ws.go | 6 +++--- vendor/modules.txt | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index ce30c11..3703ecf 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/ascenmmo/tcp-server v1.0.1 github.com/ascenmmo/token-generator v1.0.0 github.com/ascenmmo/udp-server v1.0.1 - github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f + github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae github.com/go-kit/kit v0.13.0 github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.52.5 diff --git a/go.sum b/go.sum index 5dbe969..a51606b 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/ascenmmo/token-generator v1.0.0 h1:+yUCIXfeO68tuWvYyStT4dnnYWfujfP4vP github.com/ascenmmo/token-generator v1.0.0/go.mod h1:M3RdQKl1JfQeqM2hWNs0645G48GupB7idwdzQMkxlxM= github.com/ascenmmo/udp-server v1.0.1 h1:bOKY/+IJt9wesYYUh5GEmWwgSXpffN+eSKBqdL+/Hc4= github.com/ascenmmo/udp-server v1.0.1/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= -github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f h1:UaAIZI0w9igJpmSUCQDyGo5FZmJJel0F1Yuo0iK3cF8= -github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= +github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae h1:+UgiuDE3dml/mc0MT1nUCJA00NrA7OEmKa98n79ELTY= +github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go b/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go index 313a7a5..0fc2aef 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/connection/dataSender.go @@ -1,7 +1,7 @@ package connection type DataSender interface { - Write([]byte) error + Write(msgType int, msg []byte) error GetID() string Close() } diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go b/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go index d9b5a4a..7fbe157 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/connection/ws.go @@ -18,10 +18,10 @@ func (ws *WebSocketConnection) GetID() string { return ws.Conn.RemoteAddr().String() } -func (ws *WebSocketConnection) Write(msg []byte) error { +func (ws *WebSocketConnection) Write(msgType int, msg []byte) error { ws.mutex.Lock() defer ws.mutex.Unlock() - err := ws.Conn.WriteMessage(websocket.BinaryMessage, msg) + err := ws.Conn.WriteMessage(msgType, msg) return err } diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go b/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go index 1b1bbd7..334ceb0 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go @@ -84,13 +84,14 @@ func (ws *WebSocket) handleConnection(ctx context.Context, token string, clientI defer pingTicker.Stop() ctx, cancel := context.WithCancel(ctx) + ds := connection.DataSender(&connection.WebSocketConnection{Conn: conn, CtxClose: cancel}) for { select { case <-ctx.Done(): return case <-pingTicker.C: - if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { + if err := ds.Write(websocket.PingMessage, []byte("ping")); err != nil { ws.logger.Error().Err(err).Msg("ping failed") return } @@ -118,7 +119,6 @@ func (ws *WebSocket) handleConnection(ctx context.Context, token string, clientI continue } - ds := connection.DataSender(&connection.WebSocketConnection{Conn: conn, CtxClose: cancel}) users, newMessage, err := ws.service.GetUsersAndMessage(ds, clientInfo, message) if err != nil { if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { @@ -128,7 +128,7 @@ func (ws *WebSocket) handleConnection(ctx context.Context, token string, clientI } for _, user := range users { - if err := user.Connection.Write(newMessage); err != nil { + if err := user.Connection.Write(messageType, newMessage); err != nil { ws.logger.Error().Err(err).Msg("failed to send message to user") if err := ws.service.RemoveUser(clientInfo, user.ID); err != nil { ws.logger.Error().Err(err).Msg("failed to remove user") diff --git a/vendor/modules.txt b/vendor/modules.txt index 63057f3..7b2e6a0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -37,7 +37,7 @@ github.com/ascenmmo/udp-server/pkg/errors github.com/ascenmmo/udp-server/pkg/start github.com/ascenmmo/udp-server/pkg/transport github.com/ascenmmo/udp-server/pkg/transport/viewer -# github.com/ascenmmo/websocket-server v0.0.0-20241109221422-52af911d4d0f +# github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae ## explicit; go 1.23.2 github.com/ascenmmo/websocket-server/env github.com/ascenmmo/websocket-server/internal/connection From 6ab0f38bfae21a23ad475cfb49274a87a0235bec Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 01:48:18 +0300 Subject: [PATCH 09/21] update ws 2 --- cmd/multiplayer/main.go | 7 ++++++- env/env.go | 1 + go.mod | 2 +- go.sum | 4 ++-- .../ascenmmo/websocket-server/internal/handler/ws/ws.go | 8 ++++---- vendor/modules.txt | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/multiplayer/main.go b/cmd/multiplayer/main.go index 0adeef2..fba3c7c 100644 --- a/cmd/multiplayer/main.go +++ b/cmd/multiplayer/main.go @@ -14,12 +14,17 @@ import ( ) func main() { - logger := zerolog.New(os.Stdout).With().Timestamp().Logger() + logger := zerolog.Logger{} + ctx := context.Background() shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, syscall.SIGINT) + if env.DebugLogs { + logger = zerolog.New(os.Stdout).With().Timestamp().Logger() + } + if env.RunMultiplayer { go start.Multiplayer(logger) } diff --git a/env/env.go b/env/env.go index 996c73d..a83062f 100644 --- a/env/env.go +++ b/env/env.go @@ -3,6 +3,7 @@ package env var ( ServerAddress = "ascenmmo.com" TokenKey = "_remember_token_mast_be_32_bytes" + DebugLogs = true ) var ( diff --git a/go.mod b/go.mod index 3703ecf..8827d4e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/ascenmmo/tcp-server v1.0.1 github.com/ascenmmo/token-generator v1.0.0 github.com/ascenmmo/udp-server v1.0.1 - github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae + github.com/ascenmmo/websocket-server v0.0.0-20241109224646-bb98a1bf038f github.com/go-kit/kit v0.13.0 github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.52.5 diff --git a/go.sum b/go.sum index a51606b..0b31116 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/ascenmmo/token-generator v1.0.0 h1:+yUCIXfeO68tuWvYyStT4dnnYWfujfP4vP github.com/ascenmmo/token-generator v1.0.0/go.mod h1:M3RdQKl1JfQeqM2hWNs0645G48GupB7idwdzQMkxlxM= github.com/ascenmmo/udp-server v1.0.1 h1:bOKY/+IJt9wesYYUh5GEmWwgSXpffN+eSKBqdL+/Hc4= github.com/ascenmmo/udp-server v1.0.1/go.mod h1:uJBaE4CgtsgKsu7DFiW9+RYy+BXjjG3+N1V52saYEqY= -github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae h1:+UgiuDE3dml/mc0MT1nUCJA00NrA7OEmKa98n79ELTY= -github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= +github.com/ascenmmo/websocket-server v0.0.0-20241109224646-bb98a1bf038f h1:ndNfi8YwvKQAw1IGkm2CychgjfI9YFDQyeOg2FdyT6s= +github.com/ascenmmo/websocket-server v0.0.0-20241109224646-bb98a1bf038f/go.mod h1:P6ISIRo2rzqMDx5YzP7ixsFKXbYMw0DeYkAPsJS5ScE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go b/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go index 334ceb0..a2f598c 100644 --- a/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go +++ b/vendor/github.com/ascenmmo/websocket-server/internal/handler/ws/ws.go @@ -75,7 +75,10 @@ func (ws *WebSocket) handleConnection(ctx context.Context, token string, clientI fmt.Println(err) } - err = ws.service.SetNewConnection(clientInfo, connection.DataSender(&connection.WebSocketConnection{Conn: conn})) + ctx, cancel := context.WithCancel(ctx) + ds := connection.DataSender(&connection.WebSocketConnection{Conn: conn, CtxClose: cancel}) + + err = ws.service.SetNewConnection(clientInfo, ds) if err != nil { ws.logger.Error().Err(err).Msg("failed to set new connection") } @@ -83,9 +86,6 @@ func (ws *WebSocket) handleConnection(ctx context.Context, token string, clientI pingTicker := time.NewTicker(pingInterval) defer pingTicker.Stop() - ctx, cancel := context.WithCancel(ctx) - ds := connection.DataSender(&connection.WebSocketConnection{Conn: conn, CtxClose: cancel}) - for { select { case <-ctx.Done(): diff --git a/vendor/modules.txt b/vendor/modules.txt index 7b2e6a0..03bae13 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -37,7 +37,7 @@ github.com/ascenmmo/udp-server/pkg/errors github.com/ascenmmo/udp-server/pkg/start github.com/ascenmmo/udp-server/pkg/transport github.com/ascenmmo/udp-server/pkg/transport/viewer -# github.com/ascenmmo/websocket-server v0.0.0-20241109223107-320f151abbae +# github.com/ascenmmo/websocket-server v0.0.0-20241109224646-bb98a1bf038f ## explicit; go 1.23.2 github.com/ascenmmo/websocket-server/env github.com/ascenmmo/websocket-server/internal/connection From 9b28285388cd20a1f02838d26b17526f6a0c7566 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 02:23:19 +0300 Subject: [PATCH 10/21] update rate limit --- env/env.go | 2 +- go.mod | 2 + go.sum | 11 + internal/errors/errors.go | 7 + internal/start/multiplatform.go | 13 +- .../fiber/v2/internal/memory/memory.go | 97 ++ .../fiber/v2/middleware/limiter/config.go | 128 ++ .../fiber/v2/middleware/limiter/limiter.go | 25 + .../v2/middleware/limiter/limiter_fixed.go | 106 ++ .../v2/middleware/limiter/limiter_sliding.go | 137 ++ .../fiber/v2/middleware/limiter/manager.go | 92 ++ .../v2/middleware/limiter/manager_msgp.go | 160 ++ vendor/github.com/philhofer/fwd/LICENSE.md | 7 + vendor/github.com/philhofer/fwd/README.md | 359 +++++ vendor/github.com/philhofer/fwd/reader.go | 383 +++++ vendor/github.com/philhofer/fwd/writer.go | 236 +++ .../philhofer/fwd/writer_appengine.go | 6 + .../github.com/philhofer/fwd/writer_tinygo.go | 19 + .../github.com/philhofer/fwd/writer_unsafe.go | 20 + vendor/github.com/tinylib/msgp/LICENSE | 8 + .../tinylib/msgp/msgp/advise_linux.go | 25 + .../tinylib/msgp/msgp/advise_other.go | 18 + .../github.com/tinylib/msgp/msgp/circular.go | 39 + vendor/github.com/tinylib/msgp/msgp/defs.go | 147 ++ vendor/github.com/tinylib/msgp/msgp/edit.go | 242 +++ vendor/github.com/tinylib/msgp/msgp/elsize.go | 128 ++ .../tinylib/msgp/msgp/elsize_default.go | 21 + .../tinylib/msgp/msgp/elsize_tinygo.go | 13 + vendor/github.com/tinylib/msgp/msgp/errors.go | 359 +++++ .../tinylib/msgp/msgp/errors_default.go | 25 + .../tinylib/msgp/msgp/errors_tinygo.go | 42 + .../github.com/tinylib/msgp/msgp/extension.go | 550 +++++++ vendor/github.com/tinylib/msgp/msgp/file.go | 93 ++ .../github.com/tinylib/msgp/msgp/file_port.go | 48 + .../github.com/tinylib/msgp/msgp/integers.go | 174 +++ vendor/github.com/tinylib/msgp/msgp/json.go | 568 +++++++ .../tinylib/msgp/msgp/json_bytes.go | 341 ++++ vendor/github.com/tinylib/msgp/msgp/number.go | 266 ++++ vendor/github.com/tinylib/msgp/msgp/purego.go | 16 + vendor/github.com/tinylib/msgp/msgp/read.go | 1374 +++++++++++++++++ .../tinylib/msgp/msgp/read_bytes.go | 1303 ++++++++++++++++ vendor/github.com/tinylib/msgp/msgp/size.go | 39 + vendor/github.com/tinylib/msgp/msgp/unsafe.go | 37 + vendor/github.com/tinylib/msgp/msgp/write.go | 813 ++++++++++ .../tinylib/msgp/msgp/write_bytes.go | 436 ++++++ vendor/modules.txt | 8 + 46 files changed, 8941 insertions(+), 2 deletions(-) create mode 100644 internal/errors/errors.go create mode 100644 vendor/github.com/gofiber/fiber/v2/internal/memory/memory.go create mode 100644 vendor/github.com/gofiber/fiber/v2/middleware/limiter/config.go create mode 100644 vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter.go create mode 100644 vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_fixed.go create mode 100644 vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_sliding.go create mode 100644 vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager.go create mode 100644 vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager_msgp.go create mode 100644 vendor/github.com/philhofer/fwd/LICENSE.md create mode 100644 vendor/github.com/philhofer/fwd/README.md create mode 100644 vendor/github.com/philhofer/fwd/reader.go create mode 100644 vendor/github.com/philhofer/fwd/writer.go create mode 100644 vendor/github.com/philhofer/fwd/writer_appengine.go create mode 100644 vendor/github.com/philhofer/fwd/writer_tinygo.go create mode 100644 vendor/github.com/philhofer/fwd/writer_unsafe.go create mode 100644 vendor/github.com/tinylib/msgp/LICENSE create mode 100644 vendor/github.com/tinylib/msgp/msgp/advise_linux.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/advise_other.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/circular.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/defs.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/edit.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/elsize.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/elsize_default.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/elsize_tinygo.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/errors.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/errors_default.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/errors_tinygo.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/extension.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/file.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/file_port.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/integers.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/json.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/json_bytes.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/number.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/purego.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/read.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/read_bytes.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/size.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/unsafe.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/write.go create mode 100644 vendor/github.com/tinylib/msgp/msgp/write_bytes.go diff --git a/env/env.go b/env/env.go index a83062f..e595fd4 100644 --- a/env/env.go +++ b/env/env.go @@ -11,7 +11,7 @@ var ( RunAdminPanel = true MultiplayerPort = "8080" MongoURL = "mongodb://username:userpassword@ascenmmo.com:27017" - MultiplayerMaxRequestPerSecond = 5 + MultiplayerMaxRequestPerSecond = 100 ) var ( diff --git a/go.mod b/go.mod index 8827d4e..5b1cbdf 100644 --- a/go.mod +++ b/go.mod @@ -37,10 +37,12 @@ require ( github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect + github.com/philhofer/fwd v1.1.2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/go.sum b/go.sum index 0b31116..75c19a5 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0 h1:uhcF5Jd7rP9DVEL10S github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0/go.mod h1:+oCZ5GXXr7KPI/DNOQORPTq5AWHfALJj9c72b0+YsEY= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -85,6 +87,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= @@ -113,11 +117,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -126,21 +133,25 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..5c69133 --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,7 @@ +package errors + +import "errors" + +var ( + ErrTooManyRequests = errors.New("too many requests") +) diff --git a/internal/start/multiplatform.go b/internal/start/multiplatform.go index 5eaf7eb..8b063bc 100644 --- a/internal/start/multiplatform.go +++ b/internal/start/multiplatform.go @@ -3,6 +3,7 @@ package start import ( "fmt" "github.com/ascenmmo/multiplayer-game-servers/env" + "github.com/ascenmmo/multiplayer-game-servers/internal/errors" "github.com/ascenmmo/multiplayer-game-servers/internal/service/access" devtools "github.com/ascenmmo/multiplayer-game-servers/internal/service/dev_tools" "github.com/ascenmmo/multiplayer-game-servers/internal/service/registration" @@ -12,6 +13,8 @@ import ( "github.com/ascenmmo/multiplayer-game-servers/pkg/transport" tokengenerator "github.com/ascenmmo/token-generator/token_generator" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/limiter" + _ "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/rs/zerolog" "html/template" "runtime" @@ -65,11 +68,19 @@ func Multiplayer(logger zerolog.Logger) { srv := transport.New(logger, services...).WithLog() + app := srv.Fiber() if env.RunAdminPanel { - app := srv.Fiber() adminPanel(app) } + app.Use(limiter.New(limiter.Config{ + Max: env.MultiplayerMaxRequestPerSecond, + Expiration: 1 * time.Second, + LimitReached: func(c *fiber.Ctx) error { + return c.Status(fiber.StatusTooManyRequests).SendString(errors.ErrTooManyRequests.Error()) + }, + })) + logger.Info().Str("bind", fmt.Sprintf("http://%s:%s", env.ServerAddress, env.MultiplayerPort)).Msg("listen on") if err := srv.Fiber().Listen(":" + env.MultiplayerPort); err != nil { logger.Panic().Err(err).Stack().Msg("server error") diff --git a/vendor/github.com/gofiber/fiber/v2/internal/memory/memory.go b/vendor/github.com/gofiber/fiber/v2/internal/memory/memory.go new file mode 100644 index 0000000..d7b053d --- /dev/null +++ b/vendor/github.com/gofiber/fiber/v2/internal/memory/memory.go @@ -0,0 +1,97 @@ +// Package memory Is a slight copy of the memory storage, but far from the storage interface it can not only work with bytes +// but directly store any kind of data without having to encode it each time, which gives a huge speed advantage +package memory + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2/utils" +) + +type Storage struct { + sync.RWMutex + data map[string]item // data +} + +type item struct { + // max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000 + e uint32 // exp + v interface{} // val +} + +func New() *Storage { + store := &Storage{ + data: make(map[string]item), + } + utils.StartTimeStampUpdater() + go store.gc(1 * time.Second) + return store +} + +// Get value by key +func (s *Storage) Get(key string) interface{} { + s.RLock() + v, ok := s.data[key] + s.RUnlock() + if !ok || v.e != 0 && v.e <= atomic.LoadUint32(&utils.Timestamp) { + return nil + } + return v.v +} + +// Set key with value +func (s *Storage) Set(key string, val interface{}, ttl time.Duration) { + var exp uint32 + if ttl > 0 { + exp = uint32(ttl.Seconds()) + atomic.LoadUint32(&utils.Timestamp) + } + i := item{exp, val} + s.Lock() + s.data[key] = i + s.Unlock() +} + +// Delete key by key +func (s *Storage) Delete(key string) { + s.Lock() + delete(s.data, key) + s.Unlock() +} + +// Reset all keys +func (s *Storage) Reset() { + nd := make(map[string]item) + s.Lock() + s.data = nd + s.Unlock() +} + +func (s *Storage) gc(sleep time.Duration) { + ticker := time.NewTicker(sleep) + defer ticker.Stop() + var expired []string + + for range ticker.C { + ts := atomic.LoadUint32(&utils.Timestamp) + expired = expired[:0] + s.RLock() + for key, v := range s.data { + if v.e != 0 && v.e <= ts { + expired = append(expired, key) + } + } + s.RUnlock() + s.Lock() + // Double-checked locking. + // We might have replaced the item in the meantime. + for i := range expired { + v := s.data[expired[i]] + if v.e != 0 && v.e <= ts { + delete(s.data, expired[i]) + } + } + s.Unlock() + } +} diff --git a/vendor/github.com/gofiber/fiber/v2/middleware/limiter/config.go b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/config.go new file mode 100644 index 0000000..5ec826d --- /dev/null +++ b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/config.go @@ -0,0 +1,128 @@ +package limiter + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Max number of recent connections during `Expiration` seconds before sending a 429 response + // + // Default: 5 + Max int + + // KeyGenerator allows you to generate custom keys, by default c.IP() is used + // + // Default: func(c *fiber.Ctx) string { + // return c.IP() + // } + KeyGenerator func(*fiber.Ctx) string + + // Expiration is the time on how long to keep records of requests in memory + // + // Default: 1 * time.Minute + Expiration time.Duration + + // LimitReached is called when a request hits the limit + // + // Default: func(c *fiber.Ctx) error { + // return c.SendStatus(fiber.StatusTooManyRequests) + // } + LimitReached fiber.Handler + + // When set to true, requests with StatusCode >= 400 won't be counted. + // + // Default: false + SkipFailedRequests bool + + // When set to true, requests with StatusCode < 400 won't be counted. + // + // Default: false + SkipSuccessfulRequests bool + + // Store is used to store the state of the middleware + // + // Default: an in memory store for this process only + Storage fiber.Storage + + // LimiterMiddleware is the struct that implements a limiter middleware. + // + // Default: a new Fixed Window Rate Limiter + LimiterMiddleware LimiterHandler + + // Deprecated: Use Expiration instead + Duration time.Duration + + // Deprecated: Use Storage instead + Store fiber.Storage + + // Deprecated: Use KeyGenerator instead + Key func(*fiber.Ctx) string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Max: 5, + Expiration: 1 * time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + return c.IP() + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTooManyRequests) + }, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: FixedWindow{}, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if int(cfg.Duration.Seconds()) > 0 { + log.Warn("[LIMITER] Duration is deprecated, please use Expiration") + cfg.Expiration = cfg.Duration + } + if cfg.Key != nil { + log.Warn("[LIMITER] Key is deprecated, please us KeyGenerator") + cfg.KeyGenerator = cfg.Key + } + if cfg.Store != nil { + log.Warn("[LIMITER] Store is deprecated, please use Storage") + cfg.Storage = cfg.Store + } + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Max <= 0 { + cfg.Max = ConfigDefault.Max + } + if int(cfg.Expiration.Seconds()) <= 0 { + cfg.Expiration = ConfigDefault.Expiration + } + if cfg.KeyGenerator == nil { + cfg.KeyGenerator = ConfigDefault.KeyGenerator + } + if cfg.LimitReached == nil { + cfg.LimitReached = ConfigDefault.LimitReached + } + if cfg.LimiterMiddleware == nil { + cfg.LimiterMiddleware = ConfigDefault.LimiterMiddleware + } + return cfg +} diff --git a/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter.go b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter.go new file mode 100644 index 0000000..b6da1e0 --- /dev/null +++ b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter.go @@ -0,0 +1,25 @@ +package limiter + +import ( + "github.com/gofiber/fiber/v2" +) + +const ( + // X-RateLimit-* headers + xRateLimitLimit = "X-RateLimit-Limit" + xRateLimitRemaining = "X-RateLimit-Remaining" + xRateLimitReset = "X-RateLimit-Reset" +) + +type LimiterHandler interface { + New(config Config) fiber.Handler +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Return the specified middleware handler. + return cfg.LimiterMiddleware.New(cfg) +} diff --git a/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_fixed.go b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_fixed.go new file mode 100644 index 0000000..b6b6d35 --- /dev/null +++ b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_fixed.go @@ -0,0 +1,106 @@ +package limiter + +import ( + "strconv" + "sync" + "sync/atomic" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +type FixedWindow struct{} + +// New creates a new fixed window middleware handler +func (FixedWindow) New(cfg Config) fiber.Handler { + var ( + // Limiter variables + mux = &sync.RWMutex{} + max = strconv.Itoa(cfg.Max) + expiration = uint64(cfg.Expiration.Seconds()) + ) + + // Create manager to simplify storage operations ( see manager.go ) + manager := newManager(cfg.Storage) + + // Update timestamp every second + utils.StartTimeStampUpdater() + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get key from request + key := cfg.KeyGenerator(c) + + // Lock entry + mux.Lock() + + // Get entry from pool and release when finished + e := manager.get(key) + + // Get timestamp + ts := uint64(atomic.LoadUint32(&utils.Timestamp)) + + // Set expiration if entry does not exist + if e.exp == 0 { + e.exp = ts + expiration + } else if ts >= e.exp { + // Check if entry is expired + e.currHits = 0 + e.exp = ts + expiration + } + + // Increment hits + e.currHits++ + + // Calculate when it resets in seconds + resetInSec := e.exp - ts + + // Set how many hits we have left + remaining := cfg.Max - e.currHits + + // Update storage + manager.set(key, e, cfg.Expiration) + + // Unlock entry + mux.Unlock() + + // Check if hits exceed the cfg.Max + if remaining < 0 { + // Return response with Retry-After header + // https://tools.ietf.org/html/rfc6584 + c.Set(fiber.HeaderRetryAfter, strconv.FormatUint(resetInSec, 10)) + + // Call LimitReached handler + return cfg.LimitReached(c) + } + + // Continue stack for reaching c.Response().StatusCode() + // Store err for returning + err := c.Next() + + // Check for SkipFailedRequests and SkipSuccessfulRequests + if (cfg.SkipSuccessfulRequests && c.Response().StatusCode() < fiber.StatusBadRequest) || + (cfg.SkipFailedRequests && c.Response().StatusCode() >= fiber.StatusBadRequest) { + // Lock entry + mux.Lock() + e = manager.get(key) + e.currHits-- + remaining++ + manager.set(key, e, cfg.Expiration) + // Unlock entry + mux.Unlock() + } + + // We can continue, update RateLimit headers + c.Set(xRateLimitLimit, max) + c.Set(xRateLimitRemaining, strconv.Itoa(remaining)) + c.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10)) + + return err + } +} diff --git a/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_sliding.go b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_sliding.go new file mode 100644 index 0000000..5043486 --- /dev/null +++ b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/limiter_sliding.go @@ -0,0 +1,137 @@ +package limiter + +import ( + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +type SlidingWindow struct{} + +// New creates a new sliding window middleware handler +func (SlidingWindow) New(cfg Config) fiber.Handler { + var ( + // Limiter variables + mux = &sync.RWMutex{} + max = strconv.Itoa(cfg.Max) + expiration = uint64(cfg.Expiration.Seconds()) + ) + + // Create manager to simplify storage operations ( see manager.go ) + manager := newManager(cfg.Storage) + + // Update timestamp every second + utils.StartTimeStampUpdater() + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get key from request + key := cfg.KeyGenerator(c) + + // Lock entry + mux.Lock() + + // Get entry from pool and release when finished + e := manager.get(key) + + // Get timestamp + ts := uint64(atomic.LoadUint32(&utils.Timestamp)) + + // Set expiration if entry does not exist + if e.exp == 0 { + e.exp = ts + expiration + } else if ts >= e.exp { + // The entry has expired, handle the expiration. + // Set the prevHits to the current hits and reset the hits to 0. + e.prevHits = e.currHits + + // Reset the current hits to 0. + e.currHits = 0 + + // Check how much into the current window it currently is and sets the + // expiry based on that, otherwise this would only reset on + // the next request and not show the correct expiry. + elapsed := ts - e.exp + if elapsed >= expiration { + e.exp = ts + expiration + } else { + e.exp = ts + expiration - elapsed + } + } + + // Increment hits + e.currHits++ + + // Calculate when it resets in seconds + resetInSec := e.exp - ts + + // weight = time until current window reset / total window length + weight := float64(resetInSec) / float64(expiration) + + // rate = request count in previous window - weight + request count in current window + rate := int(float64(e.prevHits)*weight) + e.currHits + + // Calculate how many hits can be made based on the current rate + remaining := cfg.Max - rate + + // Update storage. Garbage collect when the next window ends. + // |--------------------------|--------------------------| + // ^ ^ ^ ^ + // ts e.exp End sample window End next window + // <------------> + // resetInSec + // resetInSec = e.exp - ts - time until end of current window. + // duration + expiration = end of next window. + // Because we don't want to garbage collect in the middle of a window + // we add the expiration to the duration. + // Otherwise after the end of "sample window", attackers could launch + // a new request with the full window length. + manager.set(key, e, time.Duration(resetInSec+expiration)*time.Second) + + // Unlock entry + mux.Unlock() + + // Check if hits exceed the cfg.Max + if remaining < 0 { + // Return response with Retry-After header + // https://tools.ietf.org/html/rfc6584 + c.Set(fiber.HeaderRetryAfter, strconv.FormatUint(resetInSec, 10)) + + // Call LimitReached handler + return cfg.LimitReached(c) + } + + // Continue stack for reaching c.Response().StatusCode() + // Store err for returning + err := c.Next() + + // Check for SkipFailedRequests and SkipSuccessfulRequests + if (cfg.SkipSuccessfulRequests && c.Response().StatusCode() < fiber.StatusBadRequest) || + (cfg.SkipFailedRequests && c.Response().StatusCode() >= fiber.StatusBadRequest) { + // Lock entry + mux.Lock() + e = manager.get(key) + e.currHits-- + remaining++ + manager.set(key, e, cfg.Expiration) + // Unlock entry + mux.Unlock() + } + + // We can continue, update RateLimit headers + c.Set(xRateLimitLimit, max) + c.Set(xRateLimitRemaining, strconv.Itoa(remaining)) + c.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10)) + + return err + } +} diff --git a/vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager.go b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager.go new file mode 100644 index 0000000..374d3a1 --- /dev/null +++ b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager.go @@ -0,0 +1,92 @@ +package limiter + +import ( + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/memory" +) + +// go:generate msgp +// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported +type item struct { + currHits int + prevHits int + exp uint64 +} + +//msgp:ignore manager +type manager struct { + pool sync.Pool + memory *memory.Storage + storage fiber.Storage +} + +func newManager(storage fiber.Storage) *manager { + // Create new storage handler + manager := &manager{ + pool: sync.Pool{ + New: func() interface{} { + return new(item) + }, + }, + } + if storage != nil { + // Use provided storage if provided + manager.storage = storage + } else { + // Fallback too memory storage + manager.memory = memory.New() + } + return manager +} + +// acquire returns an *entry from the sync.Pool +func (m *manager) acquire() *item { + return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool +} + +// release and reset *entry to sync.Pool +func (m *manager) release(e *item) { + e.prevHits = 0 + e.currHits = 0 + e.exp = 0 + m.pool.Put(e) +} + +// get data from storage or memory +func (m *manager) get(key string) *item { + var it *item + if m.storage != nil { + it = m.acquire() + raw, err := m.storage.Get(key) + if err != nil { + return it + } + if raw != nil { + if _, err := it.UnmarshalMsg(raw); err != nil { + return it + } + } + return it + } + if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool + it = m.acquire() + return it + } + return it +} + +// set data to storage or memory +func (m *manager) set(key string, it *item, exp time.Duration) { + if m.storage != nil { + if raw, err := it.MarshalMsg(nil); err == nil { + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here + } + // we can release data because it's serialized to database + m.release(it) + } else { + m.memory.Set(key, it, exp) + } +} diff --git a/vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager_msgp.go b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager_msgp.go new file mode 100644 index 0000000..a0d81ec --- /dev/null +++ b/vendor/github.com/gofiber/fiber/v2/middleware/limiter/manager_msgp.go @@ -0,0 +1,160 @@ +package limiter + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "currHits": + z.currHits, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + case "prevHits": + z.prevHits, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + case "exp": + z.exp, err = dc.ReadUint64() + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z item) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 3 + // write "currHits" + err = en.Append(0x83, 0xa8, 0x63, 0x75, 0x72, 0x72, 0x48, 0x69, 0x74, 0x73) + if err != nil { + return + } + err = en.WriteInt(z.currHits) + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + // write "prevHits" + err = en.Append(0xa8, 0x70, 0x72, 0x65, 0x76, 0x48, 0x69, 0x74, 0x73) + if err != nil { + return + } + err = en.WriteInt(z.prevHits) + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + // write "exp" + err = en.Append(0xa3, 0x65, 0x78, 0x70) + if err != nil { + return + } + err = en.WriteUint64(z.exp) + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z item) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 3 + // string "currHits" + o = append(o, 0x83, 0xa8, 0x63, 0x75, 0x72, 0x72, 0x48, 0x69, 0x74, 0x73) + o = msgp.AppendInt(o, z.currHits) + // string "prevHits" + o = append(o, 0xa8, 0x70, 0x72, 0x65, 0x76, 0x48, 0x69, 0x74, 0x73) + o = msgp.AppendInt(o, z.prevHits) + // string "exp" + o = append(o, 0xa3, 0x65, 0x78, 0x70) + o = msgp.AppendUint64(o, z.exp) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "currHits": + z.currHits, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + case "prevHits": + z.prevHits, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + case "exp": + z.exp, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z item) Msgsize() (s int) { + s = 1 + 9 + msgp.IntSize + 9 + msgp.IntSize + 4 + msgp.Uint64Size + return +} diff --git a/vendor/github.com/philhofer/fwd/LICENSE.md b/vendor/github.com/philhofer/fwd/LICENSE.md new file mode 100644 index 0000000..1ac6a81 --- /dev/null +++ b/vendor/github.com/philhofer/fwd/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2014-2015, Philip Hofer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/philhofer/fwd/README.md b/vendor/github.com/philhofer/fwd/README.md new file mode 100644 index 0000000..62bd5c6 --- /dev/null +++ b/vendor/github.com/philhofer/fwd/README.md @@ -0,0 +1,359 @@ + +# fwd + +[![Go Reference](https://pkg.go.dev/badge/github.com/philhofer/fwd.svg)](https://pkg.go.dev/github.com/philhofer/fwd) + + +`import "github.com/philhofer/fwd"` + +* [Overview](#pkg-overview) +* [Index](#pkg-index) + +## Overview +Package fwd provides a buffered reader +and writer. Each has methods that help improve +the encoding/decoding performance of some binary +protocols. + +The `Writer` and `Reader` type provide similar +functionality to their counterparts in `bufio`, plus +a few extra utility methods that simplify read-ahead +and write-ahead. I wrote this package to improve serialization +performance for [github.com/tinylib/msgp](https://github.com/tinylib/msgp), +where it provided about a 2x speedup over `bufio` for certain +workloads. However, care must be taken to understand the semantics of the +extra methods provided by this package, as they allow +the user to access and manipulate the buffer memory +directly. + +The extra methods for `fwd.Reader` are `Peek`, `Skip` +and `Next`. `(*fwd.Reader).Peek`, unlike `(*bufio.Reader).Peek`, +will re-allocate the read buffer in order to accommodate arbitrarily +large read-ahead. `(*fwd.Reader).Skip` skips the next `n` bytes +in the stream, and uses the `io.Seeker` interface if the underlying +stream implements it. `(*fwd.Reader).Next` returns a slice pointing +to the next `n` bytes in the read buffer (like `Peek`), but also +increments the read position. This allows users to process streams +in arbitrary block sizes without having to manage appropriately-sized +slices. Additionally, obviating the need to copy the data from the +buffer to another location in memory can improve performance dramatically +in CPU-bound applications. + +`fwd.Writer` only has one extra method, which is `(*fwd.Writer).Next`, which +returns a slice pointing to the next `n` bytes of the writer, and increments +the write position by the length of the returned slice. This allows users +to write directly to the end of the buffer. + + + + +## Index +* [Constants](#pkg-constants) +* [type Reader](#Reader) + * [func NewReader(r io.Reader) *Reader](#NewReader) + * [func NewReaderBuf(r io.Reader, buf []byte) *Reader](#NewReaderBuf) + * [func NewReaderSize(r io.Reader, n int) *Reader](#NewReaderSize) + * [func (r *Reader) BufferSize() int](#Reader.BufferSize) + * [func (r *Reader) Buffered() int](#Reader.Buffered) + * [func (r *Reader) Next(n int) ([]byte, error)](#Reader.Next) + * [func (r *Reader) Peek(n int) ([]byte, error)](#Reader.Peek) + * [func (r *Reader) Read(b []byte) (int, error)](#Reader.Read) + * [func (r *Reader) ReadByte() (byte, error)](#Reader.ReadByte) + * [func (r *Reader) ReadFull(b []byte) (int, error)](#Reader.ReadFull) + * [func (r *Reader) Reset(rd io.Reader)](#Reader.Reset) + * [func (r *Reader) Skip(n int) (int, error)](#Reader.Skip) + * [func (r *Reader) WriteTo(w io.Writer) (int64, error)](#Reader.WriteTo) +* [type Writer](#Writer) + * [func NewWriter(w io.Writer) *Writer](#NewWriter) + * [func NewWriterBuf(w io.Writer, buf []byte) *Writer](#NewWriterBuf) + * [func NewWriterSize(w io.Writer, n int) *Writer](#NewWriterSize) + * [func (w *Writer) BufferSize() int](#Writer.BufferSize) + * [func (w *Writer) Buffered() int](#Writer.Buffered) + * [func (w *Writer) Flush() error](#Writer.Flush) + * [func (w *Writer) Next(n int) ([]byte, error)](#Writer.Next) + * [func (w *Writer) ReadFrom(r io.Reader) (int64, error)](#Writer.ReadFrom) + * [func (w *Writer) Write(p []byte) (int, error)](#Writer.Write) + * [func (w *Writer) WriteByte(b byte) error](#Writer.WriteByte) + * [func (w *Writer) WriteString(s string) (int, error)](#Writer.WriteString) + + +## Constants +``` go +const ( + // DefaultReaderSize is the default size of the read buffer + DefaultReaderSize = 2048 +) +``` +``` go +const ( + // DefaultWriterSize is the + // default write buffer size. + DefaultWriterSize = 2048 +) +``` + + + +## type Reader +``` go +type Reader struct { + // contains filtered or unexported fields +} +``` +Reader is a buffered look-ahead reader + + + + + + + + + +### func NewReader +``` go +func NewReader(r io.Reader) *Reader +``` +NewReader returns a new *Reader that reads from 'r' + + +### func NewReaderSize +``` go +func NewReaderSize(r io.Reader, n int) *Reader +``` +NewReaderSize returns a new *Reader that +reads from 'r' and has a buffer size 'n' + + + + +### func (\*Reader) BufferSize +``` go +func (r *Reader) BufferSize() int +``` +BufferSize returns the total size of the buffer + + + +### func (\*Reader) Buffered +``` go +func (r *Reader) Buffered() int +``` +Buffered returns the number of bytes currently in the buffer + + + +### func (\*Reader) Next +``` go +func (r *Reader) Next(n int) ([]byte, error) +``` +Next returns the next 'n' bytes in the stream. +Unlike Peek, Next advances the reader position. +The returned bytes point to the same +data as the buffer, so the slice is +only valid until the next reader method call. +An EOF is considered an unexpected error. +If an the returned slice is less than the +length asked for, an error will be returned, +and the reader position will not be incremented. + + + +### func (\*Reader) Peek +``` go +func (r *Reader) Peek(n int) ([]byte, error) +``` +Peek returns the next 'n' buffered bytes, +reading from the underlying reader if necessary. +It will only return a slice shorter than 'n' bytes +if it also returns an error. Peek does not advance +the reader. EOF errors are *not* returned as +io.ErrUnexpectedEOF. + + + +### func (\*Reader) Read +``` go +func (r *Reader) Read(b []byte) (int, error) +``` +Read implements `io.Reader`. + + + +### func (\*Reader) ReadByte +``` go +func (r *Reader) ReadByte() (byte, error) +``` +ReadByte implements `io.ByteReader`. + + + +### func (\*Reader) ReadFull +``` go +func (r *Reader) ReadFull(b []byte) (int, error) +``` +ReadFull attempts to read len(b) bytes into +'b'. It returns the number of bytes read into +'b', and an error if it does not return len(b). +EOF is considered an unexpected error. + + + +### func (\*Reader) Reset +``` go +func (r *Reader) Reset(rd io.Reader) +``` +Reset resets the underlying reader +and the read buffer. + + + +### func (\*Reader) Skip +``` go +func (r *Reader) Skip(n int) (int, error) +``` +Skip moves the reader forward 'n' bytes. +Returns the number of bytes skipped and any +errors encountered. It is analogous to Seek(n, 1). +If the underlying reader implements io.Seeker, then +that method will be used to skip forward. + +If the reader encounters +an EOF before skipping 'n' bytes, it +returns `io.ErrUnexpectedEOF`. If the +underlying reader implements `io.Seeker`, then +those rules apply instead. (Many implementations +will not return `io.EOF` until the next call +to Read). + + + + +### func (\*Reader) WriteTo +``` go +func (r *Reader) WriteTo(w io.Writer) (int64, error) +``` +WriteTo implements `io.WriterTo`. + + + + +## type Writer +``` go +type Writer struct { + // contains filtered or unexported fields +} + +``` +Writer is a buffered writer + + + + + + + +### func NewWriter +``` go +func NewWriter(w io.Writer) *Writer +``` +NewWriter returns a new writer +that writes to 'w' and has a buffer +that is `DefaultWriterSize` bytes. + + +### func NewWriterBuf +``` go +func NewWriterBuf(w io.Writer, buf []byte) *Writer +``` +NewWriterBuf returns a new writer +that writes to 'w' and has 'buf' as a buffer. +'buf' is not used when has smaller capacity than 18, +custom buffer is allocated instead. + + +### func NewWriterSize +``` go +func NewWriterSize(w io.Writer, n int) *Writer +``` +NewWriterSize returns a new writer that +writes to 'w' and has a buffer size 'n'. + +### func (\*Writer) BufferSize +``` go +func (w *Writer) BufferSize() int +``` +BufferSize returns the maximum size of the buffer. + + + +### func (\*Writer) Buffered +``` go +func (w *Writer) Buffered() int +``` +Buffered returns the number of buffered bytes +in the reader. + + + +### func (\*Writer) Flush +``` go +func (w *Writer) Flush() error +``` +Flush flushes any buffered bytes +to the underlying writer. + + + +### func (\*Writer) Next +``` go +func (w *Writer) Next(n int) ([]byte, error) +``` +Next returns the next 'n' free bytes +in the write buffer, flushing the writer +as necessary. Next will return `io.ErrShortBuffer` +if 'n' is greater than the size of the write buffer. +Calls to 'next' increment the write position by +the size of the returned buffer. + + + +### func (\*Writer) ReadFrom +``` go +func (w *Writer) ReadFrom(r io.Reader) (int64, error) +``` +ReadFrom implements `io.ReaderFrom` + + + +### func (\*Writer) Write +``` go +func (w *Writer) Write(p []byte) (int, error) +``` +Write implements `io.Writer` + + + +### func (\*Writer) WriteByte +``` go +func (w *Writer) WriteByte(b byte) error +``` +WriteByte implements `io.ByteWriter` + + + +### func (\*Writer) WriteString +``` go +func (w *Writer) WriteString(s string) (int, error) +``` +WriteString is analogous to Write, but it takes a string. + + + + + + + + +- - - +Generated by [godoc2md](https://github.com/davecheney/godoc2md) diff --git a/vendor/github.com/philhofer/fwd/reader.go b/vendor/github.com/philhofer/fwd/reader.go new file mode 100644 index 0000000..7c21f8f --- /dev/null +++ b/vendor/github.com/philhofer/fwd/reader.go @@ -0,0 +1,383 @@ +// Package fwd provides a buffered reader +// and writer. Each has methods that help improve +// the encoding/decoding performance of some binary +// protocols. +// +// The [Writer] and [Reader] type provide similar +// functionality to their counterparts in [bufio], plus +// a few extra utility methods that simplify read-ahead +// and write-ahead. I wrote this package to improve serialization +// performance for http://github.com/tinylib/msgp, +// where it provided about a 2x speedup over `bufio` for certain +// workloads. However, care must be taken to understand the semantics of the +// extra methods provided by this package, as they allow +// the user to access and manipulate the buffer memory +// directly. +// +// The extra methods for [Reader] are [Reader.Peek], [Reader.Skip] +// and [Reader.Next]. (*fwd.Reader).Peek, unlike (*bufio.Reader).Peek, +// will re-allocate the read buffer in order to accommodate arbitrarily +// large read-ahead. (*fwd.Reader).Skip skips the next 'n' bytes +// in the stream, and uses the [io.Seeker] interface if the underlying +// stream implements it. (*fwd.Reader).Next returns a slice pointing +// to the next 'n' bytes in the read buffer (like Reader.Peek), but also +// increments the read position. This allows users to process streams +// in arbitrary block sizes without having to manage appropriately-sized +// slices. Additionally, obviating the need to copy the data from the +// buffer to another location in memory can improve performance dramatically +// in CPU-bound applications. +// +// [Writer] only has one extra method, which is (*fwd.Writer).Next, which +// returns a slice pointing to the next 'n' bytes of the writer, and increments +// the write position by the length of the returned slice. This allows users +// to write directly to the end of the buffer. +package fwd + +import ( + "io" + "os" +) + +const ( + // DefaultReaderSize is the default size of the read buffer + DefaultReaderSize = 2048 + + // minimum read buffer; straight from bufio + minReaderSize = 16 +) + +// NewReader returns a new *Reader that reads from 'r' +func NewReader(r io.Reader) *Reader { + return NewReaderSize(r, DefaultReaderSize) +} + +// NewReaderSize returns a new *Reader that +// reads from 'r' and has a buffer size 'n'. +func NewReaderSize(r io.Reader, n int) *Reader { + buf := make([]byte, 0, max(n, minReaderSize)) + return NewReaderBuf(r, buf) +} + +// NewReaderBuf returns a new *Reader that +// reads from 'r' and uses 'buf' as a buffer. +// 'buf' is not used when has smaller capacity than 16, +// custom buffer is allocated instead. +func NewReaderBuf(r io.Reader, buf []byte) *Reader { + if cap(buf) < minReaderSize { + buf = make([]byte, 0, minReaderSize) + } + buf = buf[:0] + rd := &Reader{ + r: r, + data: buf, + } + if s, ok := r.(io.Seeker); ok { + rd.rs = s + } + return rd +} + +// Reader is a buffered look-ahead reader +type Reader struct { + r io.Reader // underlying reader + + // data[n:len(data)] is buffered data; data[len(data):cap(data)] is free buffer space + data []byte // data + n int // read offset + state error // last read error + + // if the reader past to NewReader was + // also an io.Seeker, this is non-nil + rs io.Seeker +} + +// Reset resets the underlying reader +// and the read buffer. +func (r *Reader) Reset(rd io.Reader) { + r.r = rd + r.data = r.data[0:0] + r.n = 0 + r.state = nil + if s, ok := rd.(io.Seeker); ok { + r.rs = s + } else { + r.rs = nil + } +} + +// more() does one read on the underlying reader +func (r *Reader) more() { + // move data backwards so that + // the read offset is 0; this way + // we can supply the maximum number of + // bytes to the reader + if r.n != 0 { + if r.n < len(r.data) { + r.data = r.data[:copy(r.data[0:], r.data[r.n:])] + } else { + r.data = r.data[:0] + } + r.n = 0 + } + var a int + a, r.state = r.r.Read(r.data[len(r.data):cap(r.data)]) + if a == 0 && r.state == nil { + r.state = io.ErrNoProgress + return + } else if a > 0 && r.state == io.EOF { + // discard the io.EOF if we read more than 0 bytes. + // the next call to Read should return io.EOF again. + r.state = nil + } else if r.state != nil { + return + } + r.data = r.data[:len(r.data)+a] +} + +// pop error +func (r *Reader) err() (e error) { + e, r.state = r.state, nil + return +} + +// pop error; EOF -> io.ErrUnexpectedEOF +func (r *Reader) noEOF() (e error) { + e, r.state = r.state, nil + if e == io.EOF { + e = io.ErrUnexpectedEOF + } + return +} + +// buffered bytes +func (r *Reader) buffered() int { return len(r.data) - r.n } + +// Buffered returns the number of bytes currently in the buffer +func (r *Reader) Buffered() int { return len(r.data) - r.n } + +// BufferSize returns the total size of the buffer +func (r *Reader) BufferSize() int { return cap(r.data) } + +// Peek returns the next 'n' buffered bytes, +// reading from the underlying reader if necessary. +// It will only return a slice shorter than 'n' bytes +// if it also returns an error. Peek does not advance +// the reader. EOF errors are *not* returned as +// io.ErrUnexpectedEOF. +func (r *Reader) Peek(n int) ([]byte, error) { + // in the degenerate case, + // we may need to realloc + // (the caller asked for more + // bytes than the size of the buffer) + if cap(r.data) < n { + old := r.data[r.n:] + r.data = make([]byte, n+r.buffered()) + r.data = r.data[:copy(r.data, old)] + r.n = 0 + } + + // keep filling until + // we hit an error or + // read enough bytes + for r.buffered() < n && r.state == nil { + r.more() + } + + // we must have hit an error + if r.buffered() < n { + return r.data[r.n:], r.err() + } + + return r.data[r.n : r.n+n], nil +} + +// discard(n) discards up to 'n' buffered bytes, and +// and returns the number of bytes discarded +func (r *Reader) discard(n int) int { + inbuf := r.buffered() + if inbuf <= n { + r.n = 0 + r.data = r.data[:0] + return inbuf + } + r.n += n + return n +} + +// Skip moves the reader forward 'n' bytes. +// Returns the number of bytes skipped and any +// errors encountered. It is analogous to Seek(n, 1). +// If the underlying reader implements io.Seeker, then +// that method will be used to skip forward. +// +// If the reader encounters +// an EOF before skipping 'n' bytes, it +// returns [io.ErrUnexpectedEOF]. If the +// underlying reader implements [io.Seeker], then +// those rules apply instead. (Many implementations +// will not return [io.EOF] until the next call +// to Read). +func (r *Reader) Skip(n int) (int, error) { + if n < 0 { + return 0, os.ErrInvalid + } + + // discard some or all of the current buffer + skipped := r.discard(n) + + // if we can Seek() through the remaining bytes, do that + if n > skipped && r.rs != nil { + nn, err := r.rs.Seek(int64(n-skipped), 1) + return int(nn) + skipped, err + } + // otherwise, keep filling the buffer + // and discarding it up to 'n' + for skipped < n && r.state == nil { + r.more() + skipped += r.discard(n - skipped) + } + return skipped, r.noEOF() +} + +// Next returns the next 'n' bytes in the stream. +// Unlike Peek, Next advances the reader position. +// The returned bytes point to the same +// data as the buffer, so the slice is +// only valid until the next reader method call. +// An EOF is considered an unexpected error. +// If an the returned slice is less than the +// length asked for, an error will be returned, +// and the reader position will not be incremented. +func (r *Reader) Next(n int) ([]byte, error) { + // in case the buffer is too small + if cap(r.data) < n { + old := r.data[r.n:] + r.data = make([]byte, n+r.buffered()) + r.data = r.data[:copy(r.data, old)] + r.n = 0 + } + + // fill at least 'n' bytes + for r.buffered() < n && r.state == nil { + r.more() + } + + if r.buffered() < n { + return r.data[r.n:], r.noEOF() + } + out := r.data[r.n : r.n+n] + r.n += n + return out, nil +} + +// Read implements [io.Reader]. +func (r *Reader) Read(b []byte) (int, error) { + // if we have data in the buffer, just + // return that. + if r.buffered() != 0 { + x := copy(b, r.data[r.n:]) + r.n += x + return x, nil + } + var n int + // we have no buffered data; determine + // whether or not to buffer or call + // the underlying reader directly + if len(b) >= cap(r.data) { + n, r.state = r.r.Read(b) + } else { + r.more() + n = copy(b, r.data) + r.n = n + } + if n == 0 { + return 0, r.err() + } + return n, nil +} + +// ReadFull attempts to read len(b) bytes into +// 'b'. It returns the number of bytes read into +// 'b', and an error if it does not return len(b). +// EOF is considered an unexpected error. +func (r *Reader) ReadFull(b []byte) (int, error) { + var n int // read into b + var nn int // scratch + l := len(b) + // either read buffered data, + // or read directly for the underlying + // buffer, or fetch more buffered data. + for n < l && r.state == nil { + if r.buffered() != 0 { + nn = copy(b[n:], r.data[r.n:]) + n += nn + r.n += nn + } else if l-n > cap(r.data) { + nn, r.state = r.r.Read(b[n:]) + n += nn + } else { + r.more() + } + } + if n < l { + return n, r.noEOF() + } + return n, nil +} + +// ReadByte implements [io.ByteReader]. +func (r *Reader) ReadByte() (byte, error) { + for r.buffered() < 1 && r.state == nil { + r.more() + } + if r.buffered() < 1 { + return 0, r.err() + } + b := r.data[r.n] + r.n++ + return b, nil +} + +// WriteTo implements [io.WriterTo]. +func (r *Reader) WriteTo(w io.Writer) (int64, error) { + var ( + i int64 + ii int + err error + ) + // first, clear buffer + if r.buffered() > 0 { + ii, err = w.Write(r.data[r.n:]) + i += int64(ii) + if err != nil { + return i, err + } + r.data = r.data[0:0] + r.n = 0 + } + for r.state == nil { + // here we just do + // 1:1 reads and writes + r.more() + if r.buffered() > 0 { + ii, err = w.Write(r.data) + i += int64(ii) + if err != nil { + return i, err + } + r.data = r.data[0:0] + r.n = 0 + } + } + if r.state != io.EOF { + return i, r.err() + } + return i, nil +} + +func max(a int, b int) int { + if a < b { + return b + } + return a +} diff --git a/vendor/github.com/philhofer/fwd/writer.go b/vendor/github.com/philhofer/fwd/writer.go new file mode 100644 index 0000000..4d6ea15 --- /dev/null +++ b/vendor/github.com/philhofer/fwd/writer.go @@ -0,0 +1,236 @@ +package fwd + +import "io" + +const ( + // DefaultWriterSize is the + // default write buffer size. + DefaultWriterSize = 2048 + + minWriterSize = minReaderSize +) + +// Writer is a buffered writer +type Writer struct { + w io.Writer // writer + buf []byte // 0:len(buf) is bufered data +} + +// NewWriter returns a new writer +// that writes to 'w' and has a buffer +// that is `DefaultWriterSize` bytes. +func NewWriter(w io.Writer) *Writer { + if wr, ok := w.(*Writer); ok { + return wr + } + return &Writer{ + w: w, + buf: make([]byte, 0, DefaultWriterSize), + } +} + +// NewWriterSize returns a new writer that +// writes to 'w' and has a buffer size 'n'. +func NewWriterSize(w io.Writer, n int) *Writer { + if wr, ok := w.(*Writer); ok && cap(wr.buf) >= n { + return wr + } + buf := make([]byte, 0, max(n, minWriterSize)) + return NewWriterBuf(w, buf) +} + +// NewWriterBuf returns a new writer +// that writes to 'w' and has 'buf' as a buffer. +// 'buf' is not used when has smaller capacity than 18, +// custom buffer is allocated instead. +func NewWriterBuf(w io.Writer, buf []byte) *Writer { + if cap(buf) < minWriterSize { + buf = make([]byte, 0, minWriterSize) + } + buf = buf[:0] + return &Writer{ + w: w, + buf: buf, + } +} + +// Buffered returns the number of buffered bytes +// in the reader. +func (w *Writer) Buffered() int { return len(w.buf) } + +// BufferSize returns the maximum size of the buffer. +func (w *Writer) BufferSize() int { return cap(w.buf) } + +// Flush flushes any buffered bytes +// to the underlying writer. +func (w *Writer) Flush() error { + l := len(w.buf) + if l > 0 { + n, err := w.w.Write(w.buf) + + // if we didn't write the whole + // thing, copy the unwritten + // bytes to the beginnning of the + // buffer. + if n < l && n > 0 { + w.pushback(n) + if err == nil { + err = io.ErrShortWrite + } + } + if err != nil { + return err + } + w.buf = w.buf[:0] + return nil + } + return nil +} + +// Write implements `io.Writer` +func (w *Writer) Write(p []byte) (int, error) { + c, l, ln := cap(w.buf), len(w.buf), len(p) + avail := c - l + + // requires flush + if avail < ln { + if err := w.Flush(); err != nil { + return 0, err + } + l = len(w.buf) + } + // too big to fit in buffer; + // write directly to w.w + if c < ln { + return w.w.Write(p) + } + + // grow buf slice; copy; return + w.buf = w.buf[:l+ln] + return copy(w.buf[l:], p), nil +} + +// WriteString is analogous to Write, but it takes a string. +func (w *Writer) WriteString(s string) (int, error) { + c, l, ln := cap(w.buf), len(w.buf), len(s) + avail := c - l + + // requires flush + if avail < ln { + if err := w.Flush(); err != nil { + return 0, err + } + l = len(w.buf) + } + // too big to fit in buffer; + // write directly to w.w + // + // yes, this is unsafe. *but* + // io.Writer is not allowed + // to mutate its input or + // maintain a reference to it, + // per the spec in package io. + // + // plus, if the string is really + // too big to fit in the buffer, then + // creating a copy to write it is + // expensive (and, strictly speaking, + // unnecessary) + if c < ln { + return w.w.Write(unsafestr(s)) + } + + // grow buf slice; copy; return + w.buf = w.buf[:l+ln] + return copy(w.buf[l:], s), nil +} + +// WriteByte implements `io.ByteWriter` +func (w *Writer) WriteByte(b byte) error { + if len(w.buf) == cap(w.buf) { + if err := w.Flush(); err != nil { + return err + } + } + w.buf = append(w.buf, b) + return nil +} + +// Next returns the next 'n' free bytes +// in the write buffer, flushing the writer +// as necessary. Next will return `io.ErrShortBuffer` +// if 'n' is greater than the size of the write buffer. +// Calls to 'next' increment the write position by +// the size of the returned buffer. +func (w *Writer) Next(n int) ([]byte, error) { + c, l := cap(w.buf), len(w.buf) + if n > c { + return nil, io.ErrShortBuffer + } + avail := c - l + if avail < n { + if err := w.Flush(); err != nil { + return nil, err + } + l = len(w.buf) + } + w.buf = w.buf[:l+n] + return w.buf[l:], nil +} + +// take the bytes from w.buf[n:len(w.buf)] +// and put them at the beginning of w.buf, +// and resize to the length of the copied segment. +func (w *Writer) pushback(n int) { + w.buf = w.buf[:copy(w.buf, w.buf[n:])] +} + +// ReadFrom implements `io.ReaderFrom` +func (w *Writer) ReadFrom(r io.Reader) (int64, error) { + // anticipatory flush + if err := w.Flush(); err != nil { + return 0, err + } + + w.buf = w.buf[0:cap(w.buf)] // expand buffer + + var nn int64 // written + var err error // error + var x int // read + + // 1:1 reads and writes + for err == nil { + x, err = r.Read(w.buf) + if x > 0 { + n, werr := w.w.Write(w.buf[:x]) + nn += int64(n) + + if err != nil { + if n < x && n > 0 { + w.pushback(n - x) + } + return nn, werr + } + if n < x { + w.pushback(n - x) + return nn, io.ErrShortWrite + } + } else if err == nil { + err = io.ErrNoProgress + break + } + } + if err != io.EOF { + return nn, err + } + + // we only clear here + // because we are sure + // the writes have + // succeeded. otherwise, + // we retain the data in case + // future writes succeed. + w.buf = w.buf[0:0] + + return nn, nil +} diff --git a/vendor/github.com/philhofer/fwd/writer_appengine.go b/vendor/github.com/philhofer/fwd/writer_appengine.go new file mode 100644 index 0000000..a978e3b --- /dev/null +++ b/vendor/github.com/philhofer/fwd/writer_appengine.go @@ -0,0 +1,6 @@ +//go:build appengine +// +build appengine + +package fwd + +func unsafestr(s string) []byte { return []byte(s) } diff --git a/vendor/github.com/philhofer/fwd/writer_tinygo.go b/vendor/github.com/philhofer/fwd/writer_tinygo.go new file mode 100644 index 0000000..b060faf --- /dev/null +++ b/vendor/github.com/philhofer/fwd/writer_tinygo.go @@ -0,0 +1,19 @@ +//go:build tinygo +// +build tinygo + +package fwd + +import ( + "reflect" + "unsafe" +) + +// unsafe cast string as []byte +func unsafestr(b string) []byte { + l := uintptr(len(b)) + return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Len: l, + Cap: l, + Data: (*reflect.StringHeader)(unsafe.Pointer(&b)).Data, + })) +} diff --git a/vendor/github.com/philhofer/fwd/writer_unsafe.go b/vendor/github.com/philhofer/fwd/writer_unsafe.go new file mode 100644 index 0000000..e4cb4a8 --- /dev/null +++ b/vendor/github.com/philhofer/fwd/writer_unsafe.go @@ -0,0 +1,20 @@ +//go:build !appengine && !tinygo +// +build !appengine,!tinygo + +package fwd + +import ( + "reflect" + "unsafe" +) + +// unsafe cast string as []byte +func unsafestr(s string) []byte { + var b []byte + sHdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + bHdr := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + bHdr.Data = sHdr.Data + bHdr.Len = sHdr.Len + bHdr.Cap = sHdr.Len + return b +} diff --git a/vendor/github.com/tinylib/msgp/LICENSE b/vendor/github.com/tinylib/msgp/LICENSE new file mode 100644 index 0000000..14d6042 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/LICENSE @@ -0,0 +1,8 @@ +Copyright (c) 2014 Philip Hofer +Portions Copyright (c) 2009 The Go Authors (license at http://golang.org) where indicated + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/tinylib/msgp/msgp/advise_linux.go b/vendor/github.com/tinylib/msgp/msgp/advise_linux.go new file mode 100644 index 0000000..d2a6685 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/advise_linux.go @@ -0,0 +1,25 @@ +//go:build linux && !appengine && !tinygo +// +build linux,!appengine,!tinygo + +package msgp + +import ( + "os" + "syscall" +) + +func adviseRead(mem []byte) { + syscall.Madvise(mem, syscall.MADV_SEQUENTIAL|syscall.MADV_WILLNEED) +} + +func adviseWrite(mem []byte) { + syscall.Madvise(mem, syscall.MADV_SEQUENTIAL) +} + +func fallocate(f *os.File, sz int64) error { + err := syscall.Fallocate(int(f.Fd()), 0, 0, sz) + if err == syscall.ENOTSUP { + return f.Truncate(sz) + } + return err +} diff --git a/vendor/github.com/tinylib/msgp/msgp/advise_other.go b/vendor/github.com/tinylib/msgp/msgp/advise_other.go new file mode 100644 index 0000000..1b6ed57 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/advise_other.go @@ -0,0 +1,18 @@ +//go:build (!linux && !tinygo) || appengine +// +build !linux,!tinygo appengine + +package msgp + +import ( + "os" +) + +// TODO: darwin, BSD support + +func adviseRead(mem []byte) {} + +func adviseWrite(mem []byte) {} + +func fallocate(f *os.File, sz int64) error { + return f.Truncate(sz) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/circular.go b/vendor/github.com/tinylib/msgp/msgp/circular.go new file mode 100644 index 0000000..a0434c7 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/circular.go @@ -0,0 +1,39 @@ +package msgp + +type timer interface { + StartTimer() + StopTimer() +} + +// EndlessReader is an io.Reader +// that loops over the same data +// endlessly. It is used for benchmarking. +type EndlessReader struct { + tb timer + data []byte + offset int +} + +// NewEndlessReader returns a new endless reader +func NewEndlessReader(b []byte, tb timer) *EndlessReader { + return &EndlessReader{tb: tb, data: b, offset: 0} +} + +// Read implements io.Reader. In practice, it +// always returns (len(p), nil), although it +// fills the supplied slice while the benchmark +// timer is stopped. +func (c *EndlessReader) Read(p []byte) (int, error) { + c.tb.StopTimer() + var n int + l := len(p) + m := len(c.data) + for n < l { + nn := copy(p[n:], c.data[c.offset:]) + n += nn + c.offset += nn + c.offset %= m + } + c.tb.StartTimer() + return n, nil +} diff --git a/vendor/github.com/tinylib/msgp/msgp/defs.go b/vendor/github.com/tinylib/msgp/msgp/defs.go new file mode 100644 index 0000000..e265aa4 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/defs.go @@ -0,0 +1,147 @@ +// This package is the support library for the msgp code generator (http://github.com/tinylib/msgp). +// +// This package defines the utilites used by the msgp code generator for encoding and decoding MessagePack +// from []byte and io.Reader/io.Writer types. Much of this package is devoted to helping the msgp code +// generator implement the Marshaler/Unmarshaler and Encodable/Decodable interfaces. +// +// This package defines four "families" of functions: +// - AppendXxxx() appends an object to a []byte in MessagePack encoding. +// - ReadXxxxBytes() reads an object from a []byte and returns the remaining bytes. +// - (*Writer).WriteXxxx() writes an object to the buffered *Writer type. +// - (*Reader).ReadXxxx() reads an object from a buffered *Reader type. +// +// Once a type has satisfied the `Encodable` and `Decodable` interfaces, +// it can be written and read from arbitrary `io.Writer`s and `io.Reader`s using +// +// msgp.Encode(io.Writer, msgp.Encodable) +// +// and +// +// msgp.Decode(io.Reader, msgp.Decodable) +// +// There are also methods for converting MessagePack to JSON without +// an explicit de-serialization step. +// +// For additional tips, tricks, and gotchas, please visit +// the wiki at http://github.com/tinylib/msgp +package msgp + +const ( + last4 = 0x0f + first4 = 0xf0 + last5 = 0x1f + first3 = 0xe0 + last7 = 0x7f +) + +func isfixint(b byte) bool { + return b>>7 == 0 +} + +func isnfixint(b byte) bool { + return b&first3 == mnfixint +} + +func isfixmap(b byte) bool { + return b&first4 == mfixmap +} + +func isfixarray(b byte) bool { + return b&first4 == mfixarray +} + +func isfixstr(b byte) bool { + return b&first3 == mfixstr +} + +func wfixint(u uint8) byte { + return u & last7 +} + +func rfixint(b byte) uint8 { + return b +} + +func wnfixint(i int8) byte { + return byte(i) | mnfixint +} + +func rnfixint(b byte) int8 { + return int8(b) +} + +func rfixmap(b byte) uint8 { + return b & last4 +} + +func wfixmap(u uint8) byte { + return mfixmap | (u & last4) +} + +func rfixstr(b byte) uint8 { + return b & last5 +} + +func wfixstr(u uint8) byte { + return (u & last5) | mfixstr +} + +func rfixarray(b byte) uint8 { + return (b & last4) +} + +func wfixarray(u uint8) byte { + return (u & last4) | mfixarray +} + +// These are all the byte +// prefixes defined by the +// msgpack standard +const ( + // 0XXXXXXX + mfixint uint8 = 0x00 + + // 111XXXXX + mnfixint uint8 = 0xe0 + + // 1000XXXX + mfixmap uint8 = 0x80 + + // 1001XXXX + mfixarray uint8 = 0x90 + + // 101XXXXX + mfixstr uint8 = 0xa0 + + mnil uint8 = 0xc0 + mfalse uint8 = 0xc2 + mtrue uint8 = 0xc3 + mbin8 uint8 = 0xc4 + mbin16 uint8 = 0xc5 + mbin32 uint8 = 0xc6 + mext8 uint8 = 0xc7 + mext16 uint8 = 0xc8 + mext32 uint8 = 0xc9 + mfloat32 uint8 = 0xca + mfloat64 uint8 = 0xcb + muint8 uint8 = 0xcc + muint16 uint8 = 0xcd + muint32 uint8 = 0xce + muint64 uint8 = 0xcf + mint8 uint8 = 0xd0 + mint16 uint8 = 0xd1 + mint32 uint8 = 0xd2 + mint64 uint8 = 0xd3 + mfixext1 uint8 = 0xd4 + mfixext2 uint8 = 0xd5 + mfixext4 uint8 = 0xd6 + mfixext8 uint8 = 0xd7 + mfixext16 uint8 = 0xd8 + mstr8 uint8 = 0xd9 + mstr16 uint8 = 0xda + mstr32 uint8 = 0xdb + marray16 uint8 = 0xdc + marray32 uint8 = 0xdd + mmap16 uint8 = 0xde + mmap32 uint8 = 0xdf +) diff --git a/vendor/github.com/tinylib/msgp/msgp/edit.go b/vendor/github.com/tinylib/msgp/msgp/edit.go new file mode 100644 index 0000000..b473a6f --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/edit.go @@ -0,0 +1,242 @@ +package msgp + +import ( + "math" +) + +// Locate returns a []byte pointing to the field +// in a messagepack map with the provided key. (The returned []byte +// points to a sub-slice of 'raw'; Locate does no allocations.) If the +// key doesn't exist in the map, a zero-length []byte will be returned. +func Locate(key string, raw []byte) []byte { + s, n := locate(raw, key) + return raw[s:n] +} + +// Replace takes a key ("key") in a messagepack map ("raw") +// and replaces its value with the one provided and returns +// the new []byte. The returned []byte may point to the same +// memory as "raw". Replace makes no effort to evaluate the validity +// of the contents of 'val'. It may use up to the full capacity of 'raw.' +// Replace returns 'nil' if the field doesn't exist or if the object in 'raw' +// is not a map. +func Replace(key string, raw []byte, val []byte) []byte { + start, end := locate(raw, key) + if start == end { + return nil + } + return replace(raw, start, end, val, true) +} + +// CopyReplace works similarly to Replace except that the returned +// byte slice does not point to the same memory as 'raw'. CopyReplace +// returns 'nil' if the field doesn't exist or 'raw' isn't a map. +func CopyReplace(key string, raw []byte, val []byte) []byte { + start, end := locate(raw, key) + if start == end { + return nil + } + return replace(raw, start, end, val, false) +} + +// Remove removes a key-value pair from 'raw'. It returns +// 'raw' unchanged if the key didn't exist. +func Remove(key string, raw []byte) []byte { + start, end := locateKV(raw, key) + if start == end { + return raw + } + raw = raw[:start+copy(raw[start:], raw[end:])] + return resizeMap(raw, -1) +} + +// HasKey returns whether the map in 'raw' has +// a field with key 'key' +func HasKey(key string, raw []byte) bool { + sz, bts, err := ReadMapHeaderBytes(raw) + if err != nil { + return false + } + var field []byte + for i := uint32(0); i < sz; i++ { + field, bts, err = ReadStringZC(bts) + if err != nil { + return false + } + if UnsafeString(field) == key { + return true + } + } + return false +} + +func replace(raw []byte, start int, end int, val []byte, inplace bool) []byte { + ll := end - start // length of segment to replace + lv := len(val) + + if inplace { + extra := lv - ll + + // fastest case: we're doing + // a 1:1 replacement + if extra == 0 { + copy(raw[start:], val) + return raw + + } else if extra < 0 { + // 'val' smaller than replaced value + // copy in place and shift back + + x := copy(raw[start:], val) + y := copy(raw[start+x:], raw[end:]) + return raw[:start+x+y] + + } else if extra < cap(raw)-len(raw) { + // 'val' less than (cap-len) extra bytes + // copy in place and shift forward + raw = raw[0 : len(raw)+extra] + // shift end forward + copy(raw[end+extra:], raw[end:]) + copy(raw[start:], val) + return raw + } + } + + // we have to allocate new space + out := make([]byte, len(raw)+len(val)-ll) + x := copy(out, raw[:start]) + y := copy(out[x:], val) + copy(out[x+y:], raw[end:]) + return out +} + +// locate does a naive O(n) search for the map key; returns start, end +// (returns 0,0 on error) +func locate(raw []byte, key string) (start int, end int) { + var ( + sz uint32 + bts []byte + field []byte + err error + ) + sz, bts, err = ReadMapHeaderBytes(raw) + if err != nil { + return + } + + // loop and locate field + for i := uint32(0); i < sz; i++ { + field, bts, err = ReadStringZC(bts) + if err != nil { + return 0, 0 + } + if UnsafeString(field) == key { + // start location + l := len(raw) + start = l - len(bts) + bts, err = Skip(bts) + if err != nil { + return 0, 0 + } + end = l - len(bts) + return + } + bts, err = Skip(bts) + if err != nil { + return 0, 0 + } + } + return 0, 0 +} + +// locate key AND value +func locateKV(raw []byte, key string) (start int, end int) { + var ( + sz uint32 + bts []byte + field []byte + err error + ) + sz, bts, err = ReadMapHeaderBytes(raw) + if err != nil { + return 0, 0 + } + + for i := uint32(0); i < sz; i++ { + tmp := len(bts) + field, bts, err = ReadStringZC(bts) + if err != nil { + return 0, 0 + } + if UnsafeString(field) == key { + start = len(raw) - tmp + bts, err = Skip(bts) + if err != nil { + return 0, 0 + } + end = len(raw) - len(bts) + return + } + bts, err = Skip(bts) + if err != nil { + return 0, 0 + } + } + return 0, 0 +} + +// delta is delta on map size +func resizeMap(raw []byte, delta int64) []byte { + var sz int64 + switch raw[0] { + case mmap16: + sz = int64(big.Uint16(raw[1:])) + if sz+delta <= math.MaxUint16 { + big.PutUint16(raw[1:], uint16(sz+delta)) + return raw + } + if cap(raw)-len(raw) >= 2 { + raw = raw[0 : len(raw)+2] + copy(raw[5:], raw[3:]) + raw[0] = mmap32 + big.PutUint32(raw[1:], uint32(sz+delta)) + return raw + } + n := make([]byte, 0, len(raw)+5) + n = AppendMapHeader(n, uint32(sz+delta)) + return append(n, raw[3:]...) + + case mmap32: + sz = int64(big.Uint32(raw[1:])) + big.PutUint32(raw[1:], uint32(sz+delta)) + return raw + + default: + sz = int64(rfixmap(raw[0])) + if sz+delta < 16 { + raw[0] = wfixmap(uint8(sz + delta)) + return raw + } else if sz+delta <= math.MaxUint16 { + if cap(raw)-len(raw) >= 2 { + raw = raw[0 : len(raw)+2] + copy(raw[3:], raw[1:]) + raw[0] = mmap16 + big.PutUint16(raw[1:], uint16(sz+delta)) + return raw + } + n := make([]byte, 0, len(raw)+5) + n = AppendMapHeader(n, uint32(sz+delta)) + return append(n, raw[1:]...) + } + if cap(raw)-len(raw) >= 4 { + raw = raw[0 : len(raw)+4] + copy(raw[5:], raw[1:]) + raw[0] = mmap32 + big.PutUint32(raw[1:], uint32(sz+delta)) + return raw + } + n := make([]byte, 0, len(raw)+5) + n = AppendMapHeader(n, uint32(sz+delta)) + return append(n, raw[1:]...) + } +} diff --git a/vendor/github.com/tinylib/msgp/msgp/elsize.go b/vendor/github.com/tinylib/msgp/msgp/elsize.go new file mode 100644 index 0000000..a05b0b2 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/elsize.go @@ -0,0 +1,128 @@ +package msgp + +func calcBytespec(v byte) bytespec { + // single byte values + switch v { + + case mnil: + return bytespec{size: 1, extra: constsize, typ: NilType} + case mfalse: + return bytespec{size: 1, extra: constsize, typ: BoolType} + case mtrue: + return bytespec{size: 1, extra: constsize, typ: BoolType} + case mbin8: + return bytespec{size: 2, extra: extra8, typ: BinType} + case mbin16: + return bytespec{size: 3, extra: extra16, typ: BinType} + case mbin32: + return bytespec{size: 5, extra: extra32, typ: BinType} + case mext8: + return bytespec{size: 3, extra: extra8, typ: ExtensionType} + case mext16: + return bytespec{size: 4, extra: extra16, typ: ExtensionType} + case mext32: + return bytespec{size: 6, extra: extra32, typ: ExtensionType} + case mfloat32: + return bytespec{size: 5, extra: constsize, typ: Float32Type} + case mfloat64: + return bytespec{size: 9, extra: constsize, typ: Float64Type} + case muint8: + return bytespec{size: 2, extra: constsize, typ: UintType} + case muint16: + return bytespec{size: 3, extra: constsize, typ: UintType} + case muint32: + return bytespec{size: 5, extra: constsize, typ: UintType} + case muint64: + return bytespec{size: 9, extra: constsize, typ: UintType} + case mint8: + return bytespec{size: 2, extra: constsize, typ: IntType} + case mint16: + return bytespec{size: 3, extra: constsize, typ: IntType} + case mint32: + return bytespec{size: 5, extra: constsize, typ: IntType} + case mint64: + return bytespec{size: 9, extra: constsize, typ: IntType} + case mfixext1: + return bytespec{size: 3, extra: constsize, typ: ExtensionType} + case mfixext2: + return bytespec{size: 4, extra: constsize, typ: ExtensionType} + case mfixext4: + return bytespec{size: 6, extra: constsize, typ: ExtensionType} + case mfixext8: + return bytespec{size: 10, extra: constsize, typ: ExtensionType} + case mfixext16: + return bytespec{size: 18, extra: constsize, typ: ExtensionType} + case mstr8: + return bytespec{size: 2, extra: extra8, typ: StrType} + case mstr16: + return bytespec{size: 3, extra: extra16, typ: StrType} + case mstr32: + return bytespec{size: 5, extra: extra32, typ: StrType} + case marray16: + return bytespec{size: 3, extra: array16v, typ: ArrayType} + case marray32: + return bytespec{size: 5, extra: array32v, typ: ArrayType} + case mmap16: + return bytespec{size: 3, extra: map16v, typ: MapType} + case mmap32: + return bytespec{size: 5, extra: map32v, typ: MapType} + } + + switch { + + // fixint + case v >= mfixint && v < 0x80: + return bytespec{size: 1, extra: constsize, typ: IntType} + + // fixstr gets constsize, since the prefix yields the size + case v >= mfixstr && v < 0xc0: + return bytespec{size: 1 + rfixstr(v), extra: constsize, typ: StrType} + + // fixmap + case v >= mfixmap && v < 0x90: + return bytespec{size: 1, extra: varmode(2 * rfixmap(v)), typ: MapType} + + // fixarray + case v >= mfixarray && v < 0xa0: + return bytespec{size: 1, extra: varmode(rfixarray(v)), typ: ArrayType} + + // nfixint + case v >= mnfixint && uint16(v) < 0x100: + return bytespec{size: 1, extra: constsize, typ: IntType} + + } + + // 0xC1 is unused per the spec and falls through to here, + // everything else is covered above + + return bytespec{} +} + +func getType(v byte) Type { + return getBytespec(v).typ +} + +// a valid bytespsec has +// non-zero 'size' and +// non-zero 'typ' +type bytespec struct { + size uint8 // prefix size information + extra varmode // extra size information + typ Type // type + _ byte // makes bytespec 4 bytes (yes, this matters) +} + +// size mode +// if positive, # elements for composites +type varmode int8 + +const ( + constsize varmode = 0 // constant size (size bytes + uint8(varmode) objects) + extra8 varmode = -1 // has uint8(p[1]) extra bytes + extra16 varmode = -2 // has be16(p[1:]) extra bytes + extra32 varmode = -3 // has be32(p[1:]) extra bytes + map16v varmode = -4 // use map16 + map32v varmode = -5 // use map32 + array16v varmode = -6 // use array16 + array32v varmode = -7 // use array32 +) diff --git a/vendor/github.com/tinylib/msgp/msgp/elsize_default.go b/vendor/github.com/tinylib/msgp/msgp/elsize_default.go new file mode 100644 index 0000000..e7e8b54 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/elsize_default.go @@ -0,0 +1,21 @@ +//go:build !tinygo +// +build !tinygo + +package msgp + +// size of every object on the wire, +// plus type information. gives us +// constant-time type information +// for traversing composite objects. +var sizes [256]bytespec + +func init() { + for i := 0; i < 256; i++ { + sizes[i] = calcBytespec(byte(i)) + } +} + +// getBytespec gets inlined to a simple array index +func getBytespec(v byte) bytespec { + return sizes[v] +} diff --git a/vendor/github.com/tinylib/msgp/msgp/elsize_tinygo.go b/vendor/github.com/tinylib/msgp/msgp/elsize_tinygo.go new file mode 100644 index 0000000..041f4ad --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/elsize_tinygo.go @@ -0,0 +1,13 @@ +//go:build tinygo +// +build tinygo + +package msgp + +// for tinygo, getBytespec just calls calcBytespec +// a simple/slow function with a switch statement - +// doesn't require any heap alloc, moves the space +// requirements into code instad of ram + +func getBytespec(v byte) bytespec { + return calcBytespec(v) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/errors.go b/vendor/github.com/tinylib/msgp/msgp/errors.go new file mode 100644 index 0000000..4f19359 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/errors.go @@ -0,0 +1,359 @@ +package msgp + +import ( + "reflect" + "strconv" +) + +const resumableDefault = false + +var ( + // ErrShortBytes is returned when the + // slice being decoded is too short to + // contain the contents of the message + ErrShortBytes error = errShort{} + + // this error is only returned + // if we reach code that should + // be unreachable + fatal error = errFatal{} +) + +// Error is the interface satisfied +// by all of the errors that originate +// from this package. +type Error interface { + error + + // Resumable returns whether + // or not the error means that + // the stream of data is malformed + // and the information is unrecoverable. + Resumable() bool +} + +// contextError allows msgp Error instances to be enhanced with additional +// context about their origin. +type contextError interface { + Error + + // withContext must not modify the error instance - it must clone and + // return a new error with the context added. + withContext(ctx string) error +} + +// Cause returns the underlying cause of an error that has been wrapped +// with additional context. +func Cause(e error) error { + out := e + if e, ok := e.(errWrapped); ok && e.cause != nil { + out = e.cause + } + return out +} + +// Resumable returns whether or not the error means that the stream of data is +// malformed and the information is unrecoverable. +func Resumable(e error) bool { + if e, ok := e.(Error); ok { + return e.Resumable() + } + return resumableDefault +} + +// WrapError wraps an error with additional context that allows the part of the +// serialized type that caused the problem to be identified. Underlying errors +// can be retrieved using Cause() +// +// The input error is not modified - a new error should be returned. +// +// ErrShortBytes is not wrapped with any context due to backward compatibility +// issues with the public API. +func WrapError(err error, ctx ...interface{}) error { + switch e := err.(type) { + case errShort: + return e + case contextError: + return e.withContext(ctxString(ctx)) + default: + return errWrapped{cause: err, ctx: ctxString(ctx)} + } +} + +func addCtx(ctx, add string) string { + if ctx != "" { + return add + "/" + ctx + } else { + return add + } +} + +// errWrapped allows arbitrary errors passed to WrapError to be enhanced with +// context and unwrapped with Cause() +type errWrapped struct { + cause error + ctx string +} + +func (e errWrapped) Error() string { + if e.ctx != "" { + return e.cause.Error() + " at " + e.ctx + } else { + return e.cause.Error() + } +} + +func (e errWrapped) Resumable() bool { + if e, ok := e.cause.(Error); ok { + return e.Resumable() + } + return resumableDefault +} + +// Unwrap returns the cause. +func (e errWrapped) Unwrap() error { return e.cause } + +type errShort struct{} + +func (e errShort) Error() string { return "msgp: too few bytes left to read object" } +func (e errShort) Resumable() bool { return false } + +type errFatal struct { + ctx string +} + +func (f errFatal) Error() string { + out := "msgp: fatal decoding error (unreachable code)" + if f.ctx != "" { + out += " at " + f.ctx + } + return out +} + +func (f errFatal) Resumable() bool { return false } + +func (f errFatal) withContext(ctx string) error { f.ctx = addCtx(f.ctx, ctx); return f } + +// ArrayError is an error returned +// when decoding a fix-sized array +// of the wrong size +type ArrayError struct { + Wanted uint32 + Got uint32 + ctx string +} + +// Error implements the error interface +func (a ArrayError) Error() string { + out := "msgp: wanted array of size " + strconv.Itoa(int(a.Wanted)) + "; got " + strconv.Itoa(int(a.Got)) + if a.ctx != "" { + out += " at " + a.ctx + } + return out +} + +// Resumable is always 'true' for ArrayErrors +func (a ArrayError) Resumable() bool { return true } + +func (a ArrayError) withContext(ctx string) error { a.ctx = addCtx(a.ctx, ctx); return a } + +// IntOverflow is returned when a call +// would downcast an integer to a type +// with too few bits to hold its value. +type IntOverflow struct { + Value int64 // the value of the integer + FailedBitsize int // the bit size that the int64 could not fit into + ctx string +} + +// Error implements the error interface +func (i IntOverflow) Error() string { + str := "msgp: " + strconv.FormatInt(i.Value, 10) + " overflows int" + strconv.Itoa(i.FailedBitsize) + if i.ctx != "" { + str += " at " + i.ctx + } + return str +} + +// Resumable is always 'true' for overflows +func (i IntOverflow) Resumable() bool { return true } + +func (i IntOverflow) withContext(ctx string) error { i.ctx = addCtx(i.ctx, ctx); return i } + +// UintOverflow is returned when a call +// would downcast an unsigned integer to a type +// with too few bits to hold its value +type UintOverflow struct { + Value uint64 // value of the uint + FailedBitsize int // the bit size that couldn't fit the value + ctx string +} + +// Error implements the error interface +func (u UintOverflow) Error() string { + str := "msgp: " + strconv.FormatUint(u.Value, 10) + " overflows uint" + strconv.Itoa(u.FailedBitsize) + if u.ctx != "" { + str += " at " + u.ctx + } + return str +} + +// Resumable is always 'true' for overflows +func (u UintOverflow) Resumable() bool { return true } + +func (u UintOverflow) withContext(ctx string) error { u.ctx = addCtx(u.ctx, ctx); return u } + +// UintBelowZero is returned when a call +// would cast a signed integer below zero +// to an unsigned integer. +type UintBelowZero struct { + Value int64 // value of the incoming int + ctx string +} + +// Error implements the error interface +func (u UintBelowZero) Error() string { + str := "msgp: attempted to cast int " + strconv.FormatInt(u.Value, 10) + " to unsigned" + if u.ctx != "" { + str += " at " + u.ctx + } + return str +} + +// Resumable is always 'true' for overflows +func (u UintBelowZero) Resumable() bool { return true } + +func (u UintBelowZero) withContext(ctx string) error { + u.ctx = ctx + return u +} + +// A TypeError is returned when a particular +// decoding method is unsuitable for decoding +// a particular MessagePack value. +type TypeError struct { + Method Type // Type expected by method + Encoded Type // Type actually encoded + + ctx string +} + +// Error implements the error interface +func (t TypeError) Error() string { + out := "msgp: attempted to decode type " + quoteStr(t.Encoded.String()) + " with method for " + quoteStr(t.Method.String()) + if t.ctx != "" { + out += " at " + t.ctx + } + return out +} + +// Resumable returns 'true' for TypeErrors +func (t TypeError) Resumable() bool { return true } + +func (t TypeError) withContext(ctx string) error { t.ctx = addCtx(t.ctx, ctx); return t } + +// returns either InvalidPrefixError or +// TypeError depending on whether or not +// the prefix is recognized +func badPrefix(want Type, lead byte) error { + t := getType(lead) + if t == InvalidType { + return InvalidPrefixError(lead) + } + return TypeError{Method: want, Encoded: t} +} + +// InvalidPrefixError is returned when a bad encoding +// uses a prefix that is not recognized in the MessagePack standard. +// This kind of error is unrecoverable. +type InvalidPrefixError byte + +// Error implements the error interface +func (i InvalidPrefixError) Error() string { + return "msgp: unrecognized type prefix 0x" + strconv.FormatInt(int64(i), 16) +} + +// Resumable returns 'false' for InvalidPrefixErrors +func (i InvalidPrefixError) Resumable() bool { return false } + +// ErrUnsupportedType is returned +// when a bad argument is supplied +// to a function that takes `interface{}`. +type ErrUnsupportedType struct { + T reflect.Type + + ctx string +} + +// Error implements error +func (e *ErrUnsupportedType) Error() string { + out := "msgp: type " + quoteStr(e.T.String()) + " not supported" + if e.ctx != "" { + out += " at " + e.ctx + } + return out +} + +// Resumable returns 'true' for ErrUnsupportedType +func (e *ErrUnsupportedType) Resumable() bool { return true } + +func (e *ErrUnsupportedType) withContext(ctx string) error { + o := *e + o.ctx = addCtx(o.ctx, ctx) + return &o +} + +// simpleQuoteStr is a simplified version of strconv.Quote for TinyGo, +// which takes up a lot less code space by escaping all non-ASCII +// (UTF-8) bytes with \x. Saves about 4k of code size +// (unicode tables, needed for IsPrint(), are big). +// It lives in errors.go just so we can test it in errors_test.go +func simpleQuoteStr(s string) string { + const ( + lowerhex = "0123456789abcdef" + ) + + sb := make([]byte, 0, len(s)+2) + + sb = append(sb, `"`...) + +l: // loop through string bytes (not UTF-8 characters) + for i := 0; i < len(s); i++ { + b := s[i] + // specific escape chars + switch b { + case '\\': + sb = append(sb, `\\`...) + case '"': + sb = append(sb, `\"`...) + case '\a': + sb = append(sb, `\a`...) + case '\b': + sb = append(sb, `\b`...) + case '\f': + sb = append(sb, `\f`...) + case '\n': + sb = append(sb, `\n`...) + case '\r': + sb = append(sb, `\r`...) + case '\t': + sb = append(sb, `\t`...) + case '\v': + sb = append(sb, `\v`...) + default: + // no escaping needed (printable ASCII) + if b >= 0x20 && b <= 0x7E { + sb = append(sb, b) + continue l + } + // anything else is \x + sb = append(sb, `\x`...) + sb = append(sb, lowerhex[byte(b)>>4]) + sb = append(sb, lowerhex[byte(b)&0xF]) + continue l + } + } + + sb = append(sb, `"`...) + return string(sb) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/errors_default.go b/vendor/github.com/tinylib/msgp/msgp/errors_default.go new file mode 100644 index 0000000..e45c00a --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/errors_default.go @@ -0,0 +1,25 @@ +//go:build !tinygo +// +build !tinygo + +package msgp + +import ( + "fmt" + "strconv" +) + +// ctxString converts the incoming interface{} slice into a single string. +func ctxString(ctx []interface{}) string { + out := "" + for idx, cv := range ctx { + if idx > 0 { + out += "/" + } + out += fmt.Sprintf("%v", cv) + } + return out +} + +func quoteStr(s string) string { + return strconv.Quote(s) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/errors_tinygo.go b/vendor/github.com/tinylib/msgp/msgp/errors_tinygo.go new file mode 100644 index 0000000..8691cd3 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/errors_tinygo.go @@ -0,0 +1,42 @@ +//go:build tinygo +// +build tinygo + +package msgp + +import ( + "reflect" +) + +// ctxString converts the incoming interface{} slice into a single string, +// without using fmt under tinygo +func ctxString(ctx []interface{}) string { + out := "" + for idx, cv := range ctx { + if idx > 0 { + out += "/" + } + out += ifToStr(cv) + } + return out +} + +type stringer interface { + String() string +} + +func ifToStr(i interface{}) string { + switch v := i.(type) { + case stringer: + return v.String() + case error: + return v.Error() + case string: + return v + default: + return reflect.ValueOf(i).String() + } +} + +func quoteStr(s string) string { + return simpleQuoteStr(s) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/extension.go b/vendor/github.com/tinylib/msgp/msgp/extension.go new file mode 100644 index 0000000..b5ef3a4 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/extension.go @@ -0,0 +1,550 @@ +package msgp + +import ( + "errors" + "math" + "strconv" +) + +const ( + // Complex64Extension is the extension number used for complex64 + Complex64Extension = 3 + + // Complex128Extension is the extension number used for complex128 + Complex128Extension = 4 + + // TimeExtension is the extension number used for time.Time + TimeExtension = 5 +) + +// our extensions live here +var extensionReg = make(map[int8]func() Extension) + +// RegisterExtension registers extensions so that they +// can be initialized and returned by methods that +// decode `interface{}` values. This should only +// be called during initialization. f() should return +// a newly-initialized zero value of the extension. Keep in +// mind that extensions 3, 4, and 5 are reserved for +// complex64, complex128, and time.Time, respectively, +// and that MessagePack reserves extension types from -127 to -1. +// +// For example, if you wanted to register a user-defined struct: +// +// msgp.RegisterExtension(10, func() msgp.Extension { &MyExtension{} }) +// +// RegisterExtension will panic if you call it multiple times +// with the same 'typ' argument, or if you use a reserved +// type (3, 4, or 5). +func RegisterExtension(typ int8, f func() Extension) { + switch typ { + case Complex64Extension, Complex128Extension, TimeExtension: + panic(errors.New("msgp: forbidden extension type: " + strconv.Itoa(int(typ)))) + } + if _, ok := extensionReg[typ]; ok { + panic(errors.New("msgp: RegisterExtension() called with typ " + strconv.Itoa(int(typ)) + " more than once")) + } + extensionReg[typ] = f +} + +// ExtensionTypeError is an error type returned +// when there is a mis-match between an extension type +// and the type encoded on the wire +type ExtensionTypeError struct { + Got int8 + Want int8 +} + +// Error implements the error interface +func (e ExtensionTypeError) Error() string { + return "msgp: error decoding extension: wanted type " + strconv.Itoa(int(e.Want)) + "; got type " + strconv.Itoa(int(e.Got)) +} + +// Resumable returns 'true' for ExtensionTypeErrors +func (e ExtensionTypeError) Resumable() bool { return true } + +func errExt(got int8, wanted int8) error { + return ExtensionTypeError{Got: got, Want: wanted} +} + +// Extension is the interface fulfilled +// by types that want to define their +// own binary encoding. +type Extension interface { + // ExtensionType should return + // a int8 that identifies the concrete + // type of the extension. (Types <0 are + // officially reserved by the MessagePack + // specifications.) + ExtensionType() int8 + + // Len should return the length + // of the data to be encoded + Len() int + + // MarshalBinaryTo should copy + // the data into the supplied slice, + // assuming that the slice has length Len() + MarshalBinaryTo([]byte) error + + UnmarshalBinary([]byte) error +} + +// RawExtension implements the Extension interface +type RawExtension struct { + Data []byte + Type int8 +} + +// ExtensionType implements Extension.ExtensionType, and returns r.Type +func (r *RawExtension) ExtensionType() int8 { return r.Type } + +// Len implements Extension.Len, and returns len(r.Data) +func (r *RawExtension) Len() int { return len(r.Data) } + +// MarshalBinaryTo implements Extension.MarshalBinaryTo, +// and returns a copy of r.Data +func (r *RawExtension) MarshalBinaryTo(d []byte) error { + copy(d, r.Data) + return nil +} + +// UnmarshalBinary implements Extension.UnmarshalBinary, +// and sets r.Data to the contents of the provided slice +func (r *RawExtension) UnmarshalBinary(b []byte) error { + if cap(r.Data) >= len(b) { + r.Data = r.Data[0:len(b)] + } else { + r.Data = make([]byte, len(b)) + } + copy(r.Data, b) + return nil +} + +// WriteExtension writes an extension type to the writer +func (mw *Writer) WriteExtension(e Extension) error { + l := e.Len() + var err error + switch l { + case 0: + o, err := mw.require(3) + if err != nil { + return err + } + mw.buf[o] = mext8 + mw.buf[o+1] = 0 + mw.buf[o+2] = byte(e.ExtensionType()) + case 1: + o, err := mw.require(2) + if err != nil { + return err + } + mw.buf[o] = mfixext1 + mw.buf[o+1] = byte(e.ExtensionType()) + case 2: + o, err := mw.require(2) + if err != nil { + return err + } + mw.buf[o] = mfixext2 + mw.buf[o+1] = byte(e.ExtensionType()) + case 4: + o, err := mw.require(2) + if err != nil { + return err + } + mw.buf[o] = mfixext4 + mw.buf[o+1] = byte(e.ExtensionType()) + case 8: + o, err := mw.require(2) + if err != nil { + return err + } + mw.buf[o] = mfixext8 + mw.buf[o+1] = byte(e.ExtensionType()) + case 16: + o, err := mw.require(2) + if err != nil { + return err + } + mw.buf[o] = mfixext16 + mw.buf[o+1] = byte(e.ExtensionType()) + default: + switch { + case l < math.MaxUint8: + o, err := mw.require(3) + if err != nil { + return err + } + mw.buf[o] = mext8 + mw.buf[o+1] = byte(uint8(l)) + mw.buf[o+2] = byte(e.ExtensionType()) + case l < math.MaxUint16: + o, err := mw.require(4) + if err != nil { + return err + } + mw.buf[o] = mext16 + big.PutUint16(mw.buf[o+1:], uint16(l)) + mw.buf[o+3] = byte(e.ExtensionType()) + default: + o, err := mw.require(6) + if err != nil { + return err + } + mw.buf[o] = mext32 + big.PutUint32(mw.buf[o+1:], uint32(l)) + mw.buf[o+5] = byte(e.ExtensionType()) + } + } + // we can only write directly to the + // buffer if we're sure that it + // fits the object + if l <= mw.bufsize() { + o, err := mw.require(l) + if err != nil { + return err + } + return e.MarshalBinaryTo(mw.buf[o:]) + } + // here we create a new buffer + // just large enough for the body + // and save it as the write buffer + err = mw.flush() + if err != nil { + return err + } + buf := make([]byte, l) + err = e.MarshalBinaryTo(buf) + if err != nil { + return err + } + mw.buf = buf + mw.wloc = l + return nil +} + +// peek at the extension type, assuming the next +// kind to be read is Extension +func (m *Reader) peekExtensionType() (int8, error) { + p, err := m.R.Peek(2) + if err != nil { + return 0, err + } + spec := getBytespec(p[0]) + if spec.typ != ExtensionType { + return 0, badPrefix(ExtensionType, p[0]) + } + if spec.extra == constsize { + return int8(p[1]), nil + } + size := spec.size + p, err = m.R.Peek(int(size)) + if err != nil { + return 0, err + } + return int8(p[size-1]), nil +} + +// peekExtension peeks at the extension encoding type +// (must guarantee at least 1 byte in 'b') +func peekExtension(b []byte) (int8, error) { + spec := getBytespec(b[0]) + size := spec.size + if spec.typ != ExtensionType { + return 0, badPrefix(ExtensionType, b[0]) + } + if len(b) < int(size) { + return 0, ErrShortBytes + } + // for fixed extensions, + // the type information is in + // the second byte + if spec.extra == constsize { + return int8(b[1]), nil + } + // otherwise, it's in the last + // part of the prefix + return int8(b[size-1]), nil +} + +// ReadExtension reads the next object from the reader +// as an extension. ReadExtension will fail if the next +// object in the stream is not an extension, or if +// e.Type() is not the same as the wire type. +func (m *Reader) ReadExtension(e Extension) (err error) { + var p []byte + p, err = m.R.Peek(2) + if err != nil { + return + } + lead := p[0] + var read int + var off int + switch lead { + case mfixext1: + if int8(p[1]) != e.ExtensionType() { + err = errExt(int8(p[1]), e.ExtensionType()) + return + } + p, err = m.R.Peek(3) + if err != nil { + return + } + err = e.UnmarshalBinary(p[2:]) + if err == nil { + _, err = m.R.Skip(3) + } + return + + case mfixext2: + if int8(p[1]) != e.ExtensionType() { + err = errExt(int8(p[1]), e.ExtensionType()) + return + } + p, err = m.R.Peek(4) + if err != nil { + return + } + err = e.UnmarshalBinary(p[2:]) + if err == nil { + _, err = m.R.Skip(4) + } + return + + case mfixext4: + if int8(p[1]) != e.ExtensionType() { + err = errExt(int8(p[1]), e.ExtensionType()) + return + } + p, err = m.R.Peek(6) + if err != nil { + return + } + err = e.UnmarshalBinary(p[2:]) + if err == nil { + _, err = m.R.Skip(6) + } + return + + case mfixext8: + if int8(p[1]) != e.ExtensionType() { + err = errExt(int8(p[1]), e.ExtensionType()) + return + } + p, err = m.R.Peek(10) + if err != nil { + return + } + err = e.UnmarshalBinary(p[2:]) + if err == nil { + _, err = m.R.Skip(10) + } + return + + case mfixext16: + if int8(p[1]) != e.ExtensionType() { + err = errExt(int8(p[1]), e.ExtensionType()) + return + } + p, err = m.R.Peek(18) + if err != nil { + return + } + err = e.UnmarshalBinary(p[2:]) + if err == nil { + _, err = m.R.Skip(18) + } + return + + case mext8: + p, err = m.R.Peek(3) + if err != nil { + return + } + if int8(p[2]) != e.ExtensionType() { + err = errExt(int8(p[2]), e.ExtensionType()) + return + } + read = int(uint8(p[1])) + off = 3 + + case mext16: + p, err = m.R.Peek(4) + if err != nil { + return + } + if int8(p[3]) != e.ExtensionType() { + err = errExt(int8(p[3]), e.ExtensionType()) + return + } + read = int(big.Uint16(p[1:])) + off = 4 + + case mext32: + p, err = m.R.Peek(6) + if err != nil { + return + } + if int8(p[5]) != e.ExtensionType() { + err = errExt(int8(p[5]), e.ExtensionType()) + return + } + read = int(big.Uint32(p[1:])) + off = 6 + + default: + err = badPrefix(ExtensionType, lead) + return + } + + p, err = m.R.Peek(read + off) + if err != nil { + return + } + err = e.UnmarshalBinary(p[off:]) + if err == nil { + _, err = m.R.Skip(read + off) + } + return +} + +// AppendExtension appends a MessagePack extension to the provided slice +func AppendExtension(b []byte, e Extension) ([]byte, error) { + l := e.Len() + var o []byte + var n int + switch l { + case 0: + o, n = ensure(b, 3) + o[n] = mext8 + o[n+1] = 0 + o[n+2] = byte(e.ExtensionType()) + return o[:n+3], nil + case 1: + o, n = ensure(b, 3) + o[n] = mfixext1 + o[n+1] = byte(e.ExtensionType()) + n += 2 + case 2: + o, n = ensure(b, 4) + o[n] = mfixext2 + o[n+1] = byte(e.ExtensionType()) + n += 2 + case 4: + o, n = ensure(b, 6) + o[n] = mfixext4 + o[n+1] = byte(e.ExtensionType()) + n += 2 + case 8: + o, n = ensure(b, 10) + o[n] = mfixext8 + o[n+1] = byte(e.ExtensionType()) + n += 2 + case 16: + o, n = ensure(b, 18) + o[n] = mfixext16 + o[n+1] = byte(e.ExtensionType()) + n += 2 + default: + switch { + case l < math.MaxUint8: + o, n = ensure(b, l+3) + o[n] = mext8 + o[n+1] = byte(uint8(l)) + o[n+2] = byte(e.ExtensionType()) + n += 3 + case l < math.MaxUint16: + o, n = ensure(b, l+4) + o[n] = mext16 + big.PutUint16(o[n+1:], uint16(l)) + o[n+3] = byte(e.ExtensionType()) + n += 4 + default: + o, n = ensure(b, l+6) + o[n] = mext32 + big.PutUint32(o[n+1:], uint32(l)) + o[n+5] = byte(e.ExtensionType()) + n += 6 + } + } + return o, e.MarshalBinaryTo(o[n:]) +} + +// ReadExtensionBytes reads an extension from 'b' into 'e' +// and returns any remaining bytes. +// Possible errors: +// - ErrShortBytes ('b' not long enough) +// - ExtensionTypeError{} (wire type not the same as e.Type()) +// - TypeError{} (next object not an extension) +// - InvalidPrefixError +// - An umarshal error returned from e.UnmarshalBinary +func ReadExtensionBytes(b []byte, e Extension) ([]byte, error) { + l := len(b) + if l < 3 { + return b, ErrShortBytes + } + lead := b[0] + var ( + sz int // size of 'data' + off int // offset of 'data' + typ int8 + ) + switch lead { + case mfixext1: + typ = int8(b[1]) + sz = 1 + off = 2 + case mfixext2: + typ = int8(b[1]) + sz = 2 + off = 2 + case mfixext4: + typ = int8(b[1]) + sz = 4 + off = 2 + case mfixext8: + typ = int8(b[1]) + sz = 8 + off = 2 + case mfixext16: + typ = int8(b[1]) + sz = 16 + off = 2 + case mext8: + sz = int(uint8(b[1])) + typ = int8(b[2]) + off = 3 + if sz == 0 { + return b[3:], e.UnmarshalBinary(b[3:3]) + } + case mext16: + if l < 4 { + return b, ErrShortBytes + } + sz = int(big.Uint16(b[1:])) + typ = int8(b[3]) + off = 4 + case mext32: + if l < 6 { + return b, ErrShortBytes + } + sz = int(big.Uint32(b[1:])) + typ = int8(b[5]) + off = 6 + default: + return b, badPrefix(ExtensionType, lead) + } + + if typ != e.ExtensionType() { + return b, errExt(typ, e.ExtensionType()) + } + + // the data of the extension starts + // at 'off' and is 'sz' bytes long + if len(b[off:]) < sz { + return b, ErrShortBytes + } + tot := off + sz + return b[tot:], e.UnmarshalBinary(b[off:tot]) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/file.go b/vendor/github.com/tinylib/msgp/msgp/file.go new file mode 100644 index 0000000..0f2c375 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/file.go @@ -0,0 +1,93 @@ +//go:build (linux || darwin || dragonfly || freebsd || netbsd || openbsd) && !appengine && !tinygo +// +build linux darwin dragonfly freebsd netbsd openbsd +// +build !appengine +// +build !tinygo + +package msgp + +import ( + "os" + "syscall" +) + +// ReadFile reads a file into 'dst' using +// a read-only memory mapping. Consequently, +// the file must be mmap-able, and the +// Unmarshaler should never write to +// the source memory. (Methods generated +// by the msgp tool obey that constraint, but +// user-defined implementations may not.) +// +// Reading and writing through file mappings +// is only efficient for large files; small +// files are best read and written using +// the ordinary streaming interfaces. +func ReadFile(dst Unmarshaler, file *os.File) error { + stat, err := file.Stat() + if err != nil { + return err + } + data, err := syscall.Mmap(int(file.Fd()), 0, int(stat.Size()), syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + return err + } + adviseRead(data) + _, err = dst.UnmarshalMsg(data) + uerr := syscall.Munmap(data) + if err == nil { + err = uerr + } + return err +} + +// MarshalSizer is the combination +// of the Marshaler and Sizer +// interfaces. +type MarshalSizer interface { + Marshaler + Sizer +} + +// WriteFile writes a file from 'src' using +// memory mapping. It overwrites the entire +// contents of the previous file. +// The mapping size is calculated +// using the `Msgsize()` method +// of 'src', so it must produce a result +// equal to or greater than the actual encoded +// size of the object. Otherwise, +// a fault (SIGBUS) will occur. +// +// Reading and writing through file mappings +// is only efficient for large files; small +// files are best read and written using +// the ordinary streaming interfaces. +// +// NOTE: The performance of this call +// is highly OS- and filesystem-dependent. +// Users should take care to test that this +// performs as expected in a production environment. +// (Linux users should run a kernel and filesystem +// that support fallocate(2) for the best results.) +func WriteFile(src MarshalSizer, file *os.File) error { + sz := src.Msgsize() + err := fallocate(file, int64(sz)) + if err != nil { + return err + } + data, err := syscall.Mmap(int(file.Fd()), 0, sz, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + return err + } + adviseWrite(data) + chunk := data[:0] + chunk, err = src.MarshalMsg(chunk) + if err != nil { + return err + } + uerr := syscall.Munmap(data) + if uerr != nil { + return uerr + } + return file.Truncate(int64(len(chunk))) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/file_port.go b/vendor/github.com/tinylib/msgp/msgp/file_port.go new file mode 100644 index 0000000..2bbb3ad --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/file_port.go @@ -0,0 +1,48 @@ +//go:build windows || appengine || tinygo +// +build windows appengine tinygo + +package msgp + +import ( + "io/ioutil" + "os" +) + +// MarshalSizer is the combination +// of the Marshaler and Sizer +// interfaces. +type MarshalSizer interface { + Marshaler + Sizer +} + +func ReadFile(dst Unmarshaler, file *os.File) error { + if u, ok := dst.(Decodable); ok { + return u.DecodeMsg(NewReader(file)) + } + + data, err := ioutil.ReadAll(file) + if err != nil { + return err + } + _, err = dst.UnmarshalMsg(data) + return err +} + +func WriteFile(src MarshalSizer, file *os.File) error { + if e, ok := src.(Encodable); ok { + w := NewWriter(file) + err := e.EncodeMsg(w) + if err == nil { + err = w.Flush() + } + return err + } + + raw, err := src.MarshalMsg(nil) + if err != nil { + return err + } + _, err = file.Write(raw) + return err +} diff --git a/vendor/github.com/tinylib/msgp/msgp/integers.go b/vendor/github.com/tinylib/msgp/msgp/integers.go new file mode 100644 index 0000000..f817d77 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/integers.go @@ -0,0 +1,174 @@ +package msgp + +/* ---------------------------------- + integer encoding utilities + (inline-able) + + TODO(tinylib): there are faster, + albeit non-portable solutions + to the code below. implement + byteswap? + ---------------------------------- */ + +func putMint64(b []byte, i int64) { + b[0] = mint64 + b[1] = byte(i >> 56) + b[2] = byte(i >> 48) + b[3] = byte(i >> 40) + b[4] = byte(i >> 32) + b[5] = byte(i >> 24) + b[6] = byte(i >> 16) + b[7] = byte(i >> 8) + b[8] = byte(i) +} + +func getMint64(b []byte) int64 { + return (int64(b[1]) << 56) | (int64(b[2]) << 48) | + (int64(b[3]) << 40) | (int64(b[4]) << 32) | + (int64(b[5]) << 24) | (int64(b[6]) << 16) | + (int64(b[7]) << 8) | (int64(b[8])) +} + +func putMint32(b []byte, i int32) { + b[0] = mint32 + b[1] = byte(i >> 24) + b[2] = byte(i >> 16) + b[3] = byte(i >> 8) + b[4] = byte(i) +} + +func getMint32(b []byte) int32 { + return (int32(b[1]) << 24) | (int32(b[2]) << 16) | (int32(b[3]) << 8) | (int32(b[4])) +} + +func putMint16(b []byte, i int16) { + b[0] = mint16 + b[1] = byte(i >> 8) + b[2] = byte(i) +} + +func getMint16(b []byte) (i int16) { + return (int16(b[1]) << 8) | int16(b[2]) +} + +func putMint8(b []byte, i int8) { + b[0] = mint8 + b[1] = byte(i) +} + +func getMint8(b []byte) (i int8) { + return int8(b[1]) +} + +func putMuint64(b []byte, u uint64) { + b[0] = muint64 + b[1] = byte(u >> 56) + b[2] = byte(u >> 48) + b[3] = byte(u >> 40) + b[4] = byte(u >> 32) + b[5] = byte(u >> 24) + b[6] = byte(u >> 16) + b[7] = byte(u >> 8) + b[8] = byte(u) +} + +func getMuint64(b []byte) uint64 { + return (uint64(b[1]) << 56) | (uint64(b[2]) << 48) | + (uint64(b[3]) << 40) | (uint64(b[4]) << 32) | + (uint64(b[5]) << 24) | (uint64(b[6]) << 16) | + (uint64(b[7]) << 8) | (uint64(b[8])) +} + +func putMuint32(b []byte, u uint32) { + b[0] = muint32 + b[1] = byte(u >> 24) + b[2] = byte(u >> 16) + b[3] = byte(u >> 8) + b[4] = byte(u) +} + +func getMuint32(b []byte) uint32 { + return (uint32(b[1]) << 24) | (uint32(b[2]) << 16) | (uint32(b[3]) << 8) | (uint32(b[4])) +} + +func putMuint16(b []byte, u uint16) { + b[0] = muint16 + b[1] = byte(u >> 8) + b[2] = byte(u) +} + +func getMuint16(b []byte) uint16 { + return (uint16(b[1]) << 8) | uint16(b[2]) +} + +func putMuint8(b []byte, u uint8) { + b[0] = muint8 + b[1] = byte(u) +} + +func getMuint8(b []byte) uint8 { + return uint8(b[1]) +} + +func getUnix(b []byte) (sec int64, nsec int32) { + sec = (int64(b[0]) << 56) | (int64(b[1]) << 48) | + (int64(b[2]) << 40) | (int64(b[3]) << 32) | + (int64(b[4]) << 24) | (int64(b[5]) << 16) | + (int64(b[6]) << 8) | (int64(b[7])) + + nsec = (int32(b[8]) << 24) | (int32(b[9]) << 16) | (int32(b[10]) << 8) | (int32(b[11])) + return +} + +func putUnix(b []byte, sec int64, nsec int32) { + b[0] = byte(sec >> 56) + b[1] = byte(sec >> 48) + b[2] = byte(sec >> 40) + b[3] = byte(sec >> 32) + b[4] = byte(sec >> 24) + b[5] = byte(sec >> 16) + b[6] = byte(sec >> 8) + b[7] = byte(sec) + b[8] = byte(nsec >> 24) + b[9] = byte(nsec >> 16) + b[10] = byte(nsec >> 8) + b[11] = byte(nsec) +} + +/* ----------------------------- + prefix utilities + ----------------------------- */ + +// write prefix and uint8 +func prefixu8(b []byte, pre byte, sz uint8) { + b[0] = pre + b[1] = byte(sz) +} + +// write prefix and big-endian uint16 +func prefixu16(b []byte, pre byte, sz uint16) { + b[0] = pre + b[1] = byte(sz >> 8) + b[2] = byte(sz) +} + +// write prefix and big-endian uint32 +func prefixu32(b []byte, pre byte, sz uint32) { + b[0] = pre + b[1] = byte(sz >> 24) + b[2] = byte(sz >> 16) + b[3] = byte(sz >> 8) + b[4] = byte(sz) +} + +func prefixu64(b []byte, pre byte, sz uint64) { + b[0] = pre + b[1] = byte(sz >> 56) + b[2] = byte(sz >> 48) + b[3] = byte(sz >> 40) + b[4] = byte(sz >> 32) + b[5] = byte(sz >> 24) + b[6] = byte(sz >> 16) + b[7] = byte(sz >> 8) + b[8] = byte(sz) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/json.go b/vendor/github.com/tinylib/msgp/msgp/json.go new file mode 100644 index 0000000..0e11e60 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/json.go @@ -0,0 +1,568 @@ +package msgp + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "io" + "strconv" + "unicode/utf8" +) + +var ( + null = []byte("null") + hex = []byte("0123456789abcdef") +) + +var defuns [_maxtype]func(jsWriter, *Reader) (int, error) + +// note: there is an initialization loop if +// this isn't set up during init() +func init() { + // since none of these functions are inline-able, + // there is not much of a penalty to the indirect + // call. however, this is best expressed as a jump-table... + defuns = [_maxtype]func(jsWriter, *Reader) (int, error){ + StrType: rwString, + BinType: rwBytes, + MapType: rwMap, + ArrayType: rwArray, + Float64Type: rwFloat64, + Float32Type: rwFloat32, + BoolType: rwBool, + IntType: rwInt, + UintType: rwUint, + NilType: rwNil, + ExtensionType: rwExtension, + Complex64Type: rwExtension, + Complex128Type: rwExtension, + TimeType: rwTime, + } +} + +// this is the interface +// used to write json +type jsWriter interface { + io.Writer + io.ByteWriter + WriteString(string) (int, error) +} + +// CopyToJSON reads MessagePack from 'src' and copies it +// as JSON to 'dst' until EOF. +func CopyToJSON(dst io.Writer, src io.Reader) (n int64, err error) { + r := NewReader(src) + n, err = r.WriteToJSON(dst) + freeR(r) + return +} + +// WriteToJSON translates MessagePack from 'r' and writes it as +// JSON to 'w' until the underlying reader returns io.EOF. It returns +// the number of bytes written, and an error if it stopped before EOF. +func (r *Reader) WriteToJSON(w io.Writer) (n int64, err error) { + var j jsWriter + var bf *bufio.Writer + if jsw, ok := w.(jsWriter); ok { + j = jsw + } else { + bf = bufio.NewWriter(w) + j = bf + } + var nn int + for err == nil { + nn, err = rwNext(j, r) + n += int64(nn) + } + if err != io.EOF { + if bf != nil { + bf.Flush() + } + return + } + err = nil + if bf != nil { + err = bf.Flush() + } + return +} + +func rwNext(w jsWriter, src *Reader) (int, error) { + t, err := src.NextType() + if err != nil { + return 0, err + } + return defuns[t](w, src) +} + +func rwMap(dst jsWriter, src *Reader) (n int, err error) { + var comma bool + var sz uint32 + var field []byte + + sz, err = src.ReadMapHeader() + if err != nil { + return + } + + if sz == 0 { + return dst.WriteString("{}") + } + + err = dst.WriteByte('{') + if err != nil { + return + } + n++ + var nn int + for i := uint32(0); i < sz; i++ { + if comma { + err = dst.WriteByte(',') + if err != nil { + return + } + n++ + } + + field, err = src.ReadMapKeyPtr() + if err != nil { + return + } + nn, err = rwquoted(dst, field) + n += nn + if err != nil { + return + } + + err = dst.WriteByte(':') + if err != nil { + return + } + n++ + nn, err = rwNext(dst, src) + n += nn + if err != nil { + return + } + if !comma { + comma = true + } + } + + err = dst.WriteByte('}') + if err != nil { + return + } + n++ + return +} + +func rwArray(dst jsWriter, src *Reader) (n int, err error) { + err = dst.WriteByte('[') + if err != nil { + return + } + var sz uint32 + var nn int + sz, err = src.ReadArrayHeader() + if err != nil { + return + } + comma := false + for i := uint32(0); i < sz; i++ { + if comma { + err = dst.WriteByte(',') + if err != nil { + return + } + n++ + } + nn, err = rwNext(dst, src) + n += nn + if err != nil { + return + } + comma = true + } + + err = dst.WriteByte(']') + if err != nil { + return + } + n++ + return +} + +func rwNil(dst jsWriter, src *Reader) (int, error) { + err := src.ReadNil() + if err != nil { + return 0, err + } + return dst.Write(null) +} + +func rwFloat32(dst jsWriter, src *Reader) (int, error) { + f, err := src.ReadFloat32() + if err != nil { + return 0, err + } + src.scratch = strconv.AppendFloat(src.scratch[:0], float64(f), 'f', -1, 32) + return dst.Write(src.scratch) +} + +func rwFloat64(dst jsWriter, src *Reader) (int, error) { + f, err := src.ReadFloat64() + if err != nil { + return 0, err + } + src.scratch = strconv.AppendFloat(src.scratch[:0], f, 'f', -1, 64) + return dst.Write(src.scratch) +} + +func rwInt(dst jsWriter, src *Reader) (int, error) { + i, err := src.ReadInt64() + if err != nil { + return 0, err + } + src.scratch = strconv.AppendInt(src.scratch[:0], i, 10) + return dst.Write(src.scratch) +} + +func rwUint(dst jsWriter, src *Reader) (int, error) { + u, err := src.ReadUint64() + if err != nil { + return 0, err + } + src.scratch = strconv.AppendUint(src.scratch[:0], u, 10) + return dst.Write(src.scratch) +} + +func rwBool(dst jsWriter, src *Reader) (int, error) { + b, err := src.ReadBool() + if err != nil { + return 0, err + } + if b { + return dst.WriteString("true") + } + return dst.WriteString("false") +} + +func rwTime(dst jsWriter, src *Reader) (int, error) { + t, err := src.ReadTime() + if err != nil { + return 0, err + } + bts, err := t.MarshalJSON() + if err != nil { + return 0, err + } + return dst.Write(bts) +} + +func rwExtension(dst jsWriter, src *Reader) (n int, err error) { + et, err := src.peekExtensionType() + if err != nil { + return 0, err + } + + // registered extensions can override + // the JSON encoding + if j, ok := extensionReg[et]; ok { + var bts []byte + e := j() + err = src.ReadExtension(e) + if err != nil { + return + } + bts, err = json.Marshal(e) + if err != nil { + return + } + return dst.Write(bts) + } + + e := RawExtension{} + e.Type = et + err = src.ReadExtension(&e) + if err != nil { + return + } + + var nn int + err = dst.WriteByte('{') + if err != nil { + return + } + n++ + + nn, err = dst.WriteString(`"type:"`) + n += nn + if err != nil { + return + } + + src.scratch = strconv.AppendInt(src.scratch[0:0], int64(e.Type), 10) + nn, err = dst.Write(src.scratch) + n += nn + if err != nil { + return + } + + nn, err = dst.WriteString(`,"data":"`) + n += nn + if err != nil { + return + } + + enc := base64.NewEncoder(base64.StdEncoding, dst) + + nn, err = enc.Write(e.Data) + n += nn + if err != nil { + return + } + err = enc.Close() + if err != nil { + return + } + nn, err = dst.WriteString(`"}`) + n += nn + return +} + +func rwString(dst jsWriter, src *Reader) (n int, err error) { + var p []byte + p, err = src.R.Peek(1) + if err != nil { + return + } + lead := p[0] + var read int + + if isfixstr(lead) { + read = int(rfixstr(lead)) + src.R.Skip(1) + goto write + } + + switch lead { + case mstr8: + p, err = src.R.Next(2) + if err != nil { + return + } + read = int(uint8(p[1])) + case mstr16: + p, err = src.R.Next(3) + if err != nil { + return + } + read = int(big.Uint16(p[1:])) + case mstr32: + p, err = src.R.Next(5) + if err != nil { + return + } + read = int(big.Uint32(p[1:])) + default: + err = badPrefix(StrType, lead) + return + } +write: + p, err = src.R.Next(read) + if err != nil { + return + } + n, err = rwquoted(dst, p) + return +} + +func rwBytes(dst jsWriter, src *Reader) (n int, err error) { + var nn int + err = dst.WriteByte('"') + if err != nil { + return + } + n++ + src.scratch, err = src.ReadBytes(src.scratch[:0]) + if err != nil { + return + } + enc := base64.NewEncoder(base64.StdEncoding, dst) + nn, err = enc.Write(src.scratch) + n += nn + if err != nil { + return + } + err = enc.Close() + if err != nil { + return + } + err = dst.WriteByte('"') + if err != nil { + return + } + n++ + return +} + +// Below (c) The Go Authors, 2009-2014 +// Subject to the BSD-style license found at http://golang.org +// +// see: encoding/json/encode.go:(*encodeState).stringbytes() +func rwquoted(dst jsWriter, s []byte) (n int, err error) { + var nn int + err = dst.WriteByte('"') + if err != nil { + return + } + n++ + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' && b != '&' { + i++ + continue + } + if start < i { + nn, err = dst.Write(s[start:i]) + n += nn + if err != nil { + return + } + } + switch b { + case '\\', '"': + err = dst.WriteByte('\\') + if err != nil { + return + } + n++ + err = dst.WriteByte(b) + if err != nil { + return + } + n++ + case '\n': + err = dst.WriteByte('\\') + if err != nil { + return + } + n++ + err = dst.WriteByte('n') + if err != nil { + return + } + n++ + case '\r': + err = dst.WriteByte('\\') + if err != nil { + return + } + n++ + err = dst.WriteByte('r') + if err != nil { + return + } + n++ + case '\t': + err = dst.WriteByte('\\') + if err != nil { + return + } + n++ + err = dst.WriteByte('t') + if err != nil { + return + } + n++ + default: + // This encodes bytes < 0x20 except for \t, \n and \r. + // It also escapes <, >, and & + // because they can lead to security holes when + // user-controlled strings are rendered into JSON + // and served to some browsers. + nn, err = dst.WriteString(`\u00`) + n += nn + if err != nil { + return + } + err = dst.WriteByte(hex[b>>4]) + if err != nil { + return + } + n++ + err = dst.WriteByte(hex[b&0xF]) + if err != nil { + return + } + n++ + } + i++ + start = i + continue + } + c, size := utf8.DecodeRune(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + nn, err = dst.Write(s[start:i]) + n += nn + if err != nil { + return + } + } + nn, err = dst.WriteString(`\ufffd`) + n += nn + if err != nil { + return + } + i += size + start = i + continue + } + // U+2028 is LINE SEPARATOR. + // U+2029 is PARAGRAPH SEPARATOR. + // They are both technically valid characters in JSON strings, + // but don't work in JSONP, which has to be evaluated as JavaScript, + // and can lead to security holes there. It is valid JSON to + // escape them, so we do so unconditionally. + // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + if c == '\u2028' || c == '\u2029' { + if start < i { + nn, err = dst.Write(s[start:i]) + n += nn + if err != nil { + return + } + } + nn, err = dst.WriteString(`\u202`) + n += nn + if err != nil { + return + } + err = dst.WriteByte(hex[c&0xF]) + if err != nil { + return + } + n++ + i += size + start = i + continue + } + i += size + } + if start < len(s) { + nn, err = dst.Write(s[start:]) + n += nn + if err != nil { + return + } + } + err = dst.WriteByte('"') + if err != nil { + return + } + n++ + return +} diff --git a/vendor/github.com/tinylib/msgp/msgp/json_bytes.go b/vendor/github.com/tinylib/msgp/msgp/json_bytes.go new file mode 100644 index 0000000..e6162d0 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/json_bytes.go @@ -0,0 +1,341 @@ +package msgp + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "io" + "strconv" + "time" +) + +var unfuns [_maxtype]func(jsWriter, []byte, []byte) ([]byte, []byte, error) + +func init() { + // NOTE(pmh): this is best expressed as a jump table, + // but gc doesn't do that yet. revisit post-go1.5. + unfuns = [_maxtype]func(jsWriter, []byte, []byte) ([]byte, []byte, error){ + StrType: rwStringBytes, + BinType: rwBytesBytes, + MapType: rwMapBytes, + ArrayType: rwArrayBytes, + Float64Type: rwFloat64Bytes, + Float32Type: rwFloat32Bytes, + BoolType: rwBoolBytes, + IntType: rwIntBytes, + UintType: rwUintBytes, + NilType: rwNullBytes, + ExtensionType: rwExtensionBytes, + Complex64Type: rwExtensionBytes, + Complex128Type: rwExtensionBytes, + TimeType: rwTimeBytes, + } +} + +// UnmarshalAsJSON takes raw messagepack and writes +// it as JSON to 'w'. If an error is returned, the +// bytes not translated will also be returned. If +// no errors are encountered, the length of the returned +// slice will be zero. +func UnmarshalAsJSON(w io.Writer, msg []byte) ([]byte, error) { + var ( + scratch []byte + cast bool + dst jsWriter + err error + ) + if jsw, ok := w.(jsWriter); ok { + dst = jsw + cast = true + } else { + dst = bufio.NewWriterSize(w, 512) + } + for len(msg) > 0 && err == nil { + msg, scratch, err = writeNext(dst, msg, scratch) + } + if !cast && err == nil { + err = dst.(*bufio.Writer).Flush() + } + return msg, err +} + +func writeNext(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + if len(msg) < 1 { + return msg, scratch, ErrShortBytes + } + t := getType(msg[0]) + if t == InvalidType { + return msg, scratch, InvalidPrefixError(msg[0]) + } + if t == ExtensionType { + et, err := peekExtension(msg) + if err != nil { + return nil, scratch, err + } + if et == TimeExtension { + t = TimeType + } + } + return unfuns[t](w, msg, scratch) +} + +func rwArrayBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + sz, msg, err := ReadArrayHeaderBytes(msg) + if err != nil { + return msg, scratch, err + } + err = w.WriteByte('[') + if err != nil { + return msg, scratch, err + } + for i := uint32(0); i < sz; i++ { + if i != 0 { + err = w.WriteByte(',') + if err != nil { + return msg, scratch, err + } + } + msg, scratch, err = writeNext(w, msg, scratch) + if err != nil { + return msg, scratch, err + } + } + err = w.WriteByte(']') + return msg, scratch, err +} + +func rwMapBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + sz, msg, err := ReadMapHeaderBytes(msg) + if err != nil { + return msg, scratch, err + } + err = w.WriteByte('{') + if err != nil { + return msg, scratch, err + } + for i := uint32(0); i < sz; i++ { + if i != 0 { + err = w.WriteByte(',') + if err != nil { + return msg, scratch, err + } + } + msg, scratch, err = rwMapKeyBytes(w, msg, scratch) + if err != nil { + return msg, scratch, err + } + err = w.WriteByte(':') + if err != nil { + return msg, scratch, err + } + msg, scratch, err = writeNext(w, msg, scratch) + if err != nil { + return msg, scratch, err + } + } + err = w.WriteByte('}') + return msg, scratch, err +} + +func rwMapKeyBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + msg, scratch, err := rwStringBytes(w, msg, scratch) + if err != nil { + if tperr, ok := err.(TypeError); ok && tperr.Encoded == BinType { + return rwBytesBytes(w, msg, scratch) + } + } + return msg, scratch, err +} + +func rwStringBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + str, msg, err := ReadStringZC(msg) + if err != nil { + return msg, scratch, err + } + _, err = rwquoted(w, str) + return msg, scratch, err +} + +func rwBytesBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + bts, msg, err := ReadBytesZC(msg) + if err != nil { + return msg, scratch, err + } + l := base64.StdEncoding.EncodedLen(len(bts)) + if cap(scratch) >= l { + scratch = scratch[0:l] + } else { + scratch = make([]byte, l) + } + base64.StdEncoding.Encode(scratch, bts) + err = w.WriteByte('"') + if err != nil { + return msg, scratch, err + } + _, err = w.Write(scratch) + if err != nil { + return msg, scratch, err + } + err = w.WriteByte('"') + return msg, scratch, err +} + +func rwNullBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + msg, err := ReadNilBytes(msg) + if err != nil { + return msg, scratch, err + } + _, err = w.Write(null) + return msg, scratch, err +} + +func rwBoolBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + b, msg, err := ReadBoolBytes(msg) + if err != nil { + return msg, scratch, err + } + if b { + _, err = w.WriteString("true") + return msg, scratch, err + } + _, err = w.WriteString("false") + return msg, scratch, err +} + +func rwIntBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + i, msg, err := ReadInt64Bytes(msg) + if err != nil { + return msg, scratch, err + } + scratch = strconv.AppendInt(scratch[0:0], i, 10) + _, err = w.Write(scratch) + return msg, scratch, err +} + +func rwUintBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + u, msg, err := ReadUint64Bytes(msg) + if err != nil { + return msg, scratch, err + } + scratch = strconv.AppendUint(scratch[0:0], u, 10) + _, err = w.Write(scratch) + return msg, scratch, err +} + +func rwFloat32Bytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + var f float32 + var err error + f, msg, err = ReadFloat32Bytes(msg) + if err != nil { + return msg, scratch, err + } + scratch = strconv.AppendFloat(scratch[:0], float64(f), 'f', -1, 32) + _, err = w.Write(scratch) + return msg, scratch, err +} + +func rwFloat64Bytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + var f float64 + var err error + f, msg, err = ReadFloat64Bytes(msg) + if err != nil { + return msg, scratch, err + } + scratch = strconv.AppendFloat(scratch[:0], f, 'f', -1, 64) + _, err = w.Write(scratch) + return msg, scratch, err +} + +func rwTimeBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + var t time.Time + var err error + t, msg, err = ReadTimeBytes(msg) + if err != nil { + return msg, scratch, err + } + bts, err := t.MarshalJSON() + if err != nil { + return msg, scratch, err + } + _, err = w.Write(bts) + return msg, scratch, err +} + +func rwExtensionBytes(w jsWriter, msg []byte, scratch []byte) ([]byte, []byte, error) { + var err error + var et int8 + et, err = peekExtension(msg) + if err != nil { + return msg, scratch, err + } + + // if it's time.Time + if et == TimeExtension { + var tm time.Time + tm, msg, err = ReadTimeBytes(msg) + if err != nil { + return msg, scratch, err + } + bts, err := tm.MarshalJSON() + if err != nil { + return msg, scratch, err + } + _, err = w.Write(bts) + return msg, scratch, err + } + + // if the extension is registered, + // use its canonical JSON form + if f, ok := extensionReg[et]; ok { + e := f() + msg, err = ReadExtensionBytes(msg, e) + if err != nil { + return msg, scratch, err + } + bts, err := json.Marshal(e) + if err != nil { + return msg, scratch, err + } + _, err = w.Write(bts) + return msg, scratch, err + } + + // otherwise, write `{"type": , "data": ""}` + r := RawExtension{} + r.Type = et + msg, err = ReadExtensionBytes(msg, &r) + if err != nil { + return msg, scratch, err + } + scratch, err = writeExt(w, r, scratch) + return msg, scratch, err +} + +func writeExt(w jsWriter, r RawExtension, scratch []byte) ([]byte, error) { + _, err := w.WriteString(`{"type":`) + if err != nil { + return scratch, err + } + scratch = strconv.AppendInt(scratch[0:0], int64(r.Type), 10) + _, err = w.Write(scratch) + if err != nil { + return scratch, err + } + _, err = w.WriteString(`,"data":"`) + if err != nil { + return scratch, err + } + l := base64.StdEncoding.EncodedLen(len(r.Data)) + if cap(scratch) >= l { + scratch = scratch[0:l] + } else { + scratch = make([]byte, l) + } + base64.StdEncoding.Encode(scratch, r.Data) + _, err = w.Write(scratch) + if err != nil { + return scratch, err + } + _, err = w.WriteString(`"}`) + return scratch, err +} diff --git a/vendor/github.com/tinylib/msgp/msgp/number.go b/vendor/github.com/tinylib/msgp/msgp/number.go new file mode 100644 index 0000000..edfe328 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/number.go @@ -0,0 +1,266 @@ +package msgp + +import ( + "math" + "strconv" +) + +// The portable parts of the Number implementation + +// Number can be +// an int64, uint64, float32, +// or float64 internally. +// It can decode itself +// from any of the native +// messagepack number types. +// The zero-value of Number +// is Int(0). Using the equality +// operator with Number compares +// both the type and the value +// of the number. +type Number struct { + // internally, this + // is just a tagged union. + // the raw bits of the number + // are stored the same way regardless. + bits uint64 + typ Type +} + +// AsInt sets the number to an int64. +func (n *Number) AsInt(i int64) { + // we always store int(0) + // as {0, InvalidType} in + // order to preserve + // the behavior of the == operator + if i == 0 { + n.typ = InvalidType + n.bits = 0 + return + } + + n.typ = IntType + n.bits = uint64(i) +} + +// AsUint sets the number to a uint64. +func (n *Number) AsUint(u uint64) { + n.typ = UintType + n.bits = u +} + +// AsFloat32 sets the value of the number +// to a float32. +func (n *Number) AsFloat32(f float32) { + n.typ = Float32Type + n.bits = uint64(math.Float32bits(f)) +} + +// AsFloat64 sets the value of the +// number to a float64. +func (n *Number) AsFloat64(f float64) { + n.typ = Float64Type + n.bits = math.Float64bits(f) +} + +// Int casts the number as an int64, and +// returns whether or not that was the +// underlying type. +func (n *Number) Int() (int64, bool) { + return int64(n.bits), n.typ == IntType || n.typ == InvalidType +} + +// Uint casts the number as a uint64, and returns +// whether or not that was the underlying type. +func (n *Number) Uint() (uint64, bool) { + return n.bits, n.typ == UintType +} + +// Float casts the number to a float64, and +// returns whether or not that was the underlying +// type (either a float64 or a float32). +func (n *Number) Float() (float64, bool) { + switch n.typ { + case Float32Type: + return float64(math.Float32frombits(uint32(n.bits))), true + case Float64Type: + return math.Float64frombits(n.bits), true + default: + return 0.0, false + } +} + +// Type will return one of: +// Float64Type, Float32Type, UintType, or IntType. +func (n *Number) Type() Type { + if n.typ == InvalidType { + return IntType + } + return n.typ +} + +// DecodeMsg implements msgp.Decodable +func (n *Number) DecodeMsg(r *Reader) error { + typ, err := r.NextType() + if err != nil { + return err + } + switch typ { + case Float32Type: + f, err := r.ReadFloat32() + if err != nil { + return err + } + n.AsFloat32(f) + return nil + case Float64Type: + f, err := r.ReadFloat64() + if err != nil { + return err + } + n.AsFloat64(f) + return nil + case IntType: + i, err := r.ReadInt64() + if err != nil { + return err + } + n.AsInt(i) + return nil + case UintType: + u, err := r.ReadUint64() + if err != nil { + return err + } + n.AsUint(u) + return nil + default: + return TypeError{Encoded: typ, Method: IntType} + } +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (n *Number) UnmarshalMsg(b []byte) ([]byte, error) { + typ := NextType(b) + switch typ { + case IntType: + i, o, err := ReadInt64Bytes(b) + if err != nil { + return b, err + } + n.AsInt(i) + return o, nil + case UintType: + u, o, err := ReadUint64Bytes(b) + if err != nil { + return b, err + } + n.AsUint(u) + return o, nil + case Float64Type: + f, o, err := ReadFloat64Bytes(b) + if err != nil { + return b, err + } + n.AsFloat64(f) + return o, nil + case Float32Type: + f, o, err := ReadFloat32Bytes(b) + if err != nil { + return b, err + } + n.AsFloat32(f) + return o, nil + default: + return b, TypeError{Method: IntType, Encoded: typ} + } +} + +// MarshalMsg implements msgp.Marshaler +func (n *Number) MarshalMsg(b []byte) ([]byte, error) { + switch n.typ { + case IntType: + return AppendInt64(b, int64(n.bits)), nil + case UintType: + return AppendUint64(b, uint64(n.bits)), nil + case Float64Type: + return AppendFloat64(b, math.Float64frombits(n.bits)), nil + case Float32Type: + return AppendFloat32(b, math.Float32frombits(uint32(n.bits))), nil + default: + return AppendInt64(b, 0), nil + } +} + +// EncodeMsg implements msgp.Encodable +func (n *Number) EncodeMsg(w *Writer) error { + switch n.typ { + case IntType: + return w.WriteInt64(int64(n.bits)) + case UintType: + return w.WriteUint64(n.bits) + case Float64Type: + return w.WriteFloat64(math.Float64frombits(n.bits)) + case Float32Type: + return w.WriteFloat32(math.Float32frombits(uint32(n.bits))) + default: + return w.WriteInt64(0) + } +} + +// Msgsize implements msgp.Sizer +func (n *Number) Msgsize() int { + switch n.typ { + case Float32Type: + return Float32Size + case Float64Type: + return Float64Size + case IntType: + return Int64Size + case UintType: + return Uint64Size + default: + return 1 // fixint(0) + } +} + +// MarshalJSON implements json.Marshaler +func (n *Number) MarshalJSON() ([]byte, error) { + t := n.Type() + if t == InvalidType { + return []byte{'0'}, nil + } + out := make([]byte, 0, 32) + switch t { + case Float32Type, Float64Type: + f, _ := n.Float() + return strconv.AppendFloat(out, f, 'f', -1, 64), nil + case IntType: + i, _ := n.Int() + return strconv.AppendInt(out, i, 10), nil + case UintType: + u, _ := n.Uint() + return strconv.AppendUint(out, u, 10), nil + default: + panic("(*Number).typ is invalid") + } +} + +// String implements fmt.Stringer +func (n *Number) String() string { + switch n.typ { + case InvalidType: + return "0" + case Float32Type, Float64Type: + f, _ := n.Float() + return strconv.FormatFloat(f, 'f', -1, 64) + case IntType: + i, _ := n.Int() + return strconv.FormatInt(i, 10) + case UintType: + u, _ := n.Uint() + return strconv.FormatUint(u, 10) + default: + panic("(*Number).typ is invalid") + } +} diff --git a/vendor/github.com/tinylib/msgp/msgp/purego.go b/vendor/github.com/tinylib/msgp/msgp/purego.go new file mode 100644 index 0000000..2cd35c3 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/purego.go @@ -0,0 +1,16 @@ +//go:build purego || appengine +// +build purego appengine + +package msgp + +// let's just assume appengine +// uses 64-bit hardware... +const smallint = false + +func UnsafeString(b []byte) string { + return string(b) +} + +func UnsafeBytes(s string) []byte { + return []byte(s) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/read.go b/vendor/github.com/tinylib/msgp/msgp/read.go new file mode 100644 index 0000000..e6d72f1 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/read.go @@ -0,0 +1,1374 @@ +package msgp + +import ( + "io" + "math" + "sync" + "time" + + "github.com/philhofer/fwd" +) + +// where we keep old *Readers +var readerPool = sync.Pool{New: func() interface{} { return &Reader{} }} + +// Type is a MessagePack wire type, +// including this package's built-in +// extension types. +type Type byte + +// MessagePack Types +// +// The zero value of Type +// is InvalidType. +const ( + InvalidType Type = iota + + // MessagePack built-in types + + StrType + BinType + MapType + ArrayType + Float64Type + Float32Type + BoolType + IntType + UintType + NilType + DurationType + ExtensionType + + // pseudo-types provided + // by extensions + + Complex64Type + Complex128Type + TimeType + + _maxtype +) + +// String implements fmt.Stringer +func (t Type) String() string { + switch t { + case StrType: + return "str" + case BinType: + return "bin" + case MapType: + return "map" + case ArrayType: + return "array" + case Float64Type: + return "float64" + case Float32Type: + return "float32" + case BoolType: + return "bool" + case UintType: + return "uint" + case IntType: + return "int" + case ExtensionType: + return "ext" + case NilType: + return "nil" + default: + return "" + } +} + +func freeR(m *Reader) { + readerPool.Put(m) +} + +// Unmarshaler is the interface fulfilled +// by objects that know how to unmarshal +// themselves from MessagePack. +// UnmarshalMsg unmarshals the object +// from binary, returing any leftover +// bytes and any errors encountered. +type Unmarshaler interface { + UnmarshalMsg([]byte) ([]byte, error) +} + +// Decodable is the interface fulfilled +// by objects that know how to read +// themselves from a *Reader. +type Decodable interface { + DecodeMsg(*Reader) error +} + +// Decode decodes 'd' from 'r'. +func Decode(r io.Reader, d Decodable) error { + rd := NewReader(r) + err := d.DecodeMsg(rd) + freeR(rd) + return err +} + +// NewReader returns a *Reader that +// reads from the provided reader. The +// reader will be buffered. +func NewReader(r io.Reader) *Reader { + p := readerPool.Get().(*Reader) + if p.R == nil { + p.R = fwd.NewReader(r) + } else { + p.R.Reset(r) + } + return p +} + +// NewReaderSize returns a *Reader with a buffer of the given size. +// (This is vastly preferable to passing the decoder a reader that is already buffered.) +func NewReaderSize(r io.Reader, sz int) *Reader { + return &Reader{R: fwd.NewReaderSize(r, sz)} +} + +// NewReaderBuf returns a *Reader with a provided buffer. +func NewReaderBuf(r io.Reader, buf []byte) *Reader { + return &Reader{R: fwd.NewReaderBuf(r, buf)} +} + +// Reader wraps an io.Reader and provides +// methods to read MessagePack-encoded values +// from it. Readers are buffered. +type Reader struct { + // R is the buffered reader + // that the Reader uses + // to decode MessagePack. + // The Reader itself + // is stateless; all the + // buffering is done + // within R. + R *fwd.Reader + scratch []byte +} + +// Read implements `io.Reader` +func (m *Reader) Read(p []byte) (int, error) { + return m.R.Read(p) +} + +// CopyNext reads the next object from m without decoding it and writes it to w. +// It avoids unnecessary copies internally. +func (m *Reader) CopyNext(w io.Writer) (int64, error) { + sz, o, err := getNextSize(m.R) + if err != nil { + return 0, err + } + + var n int64 + // Opportunistic optimization: if we can fit the whole thing in the m.R + // buffer, then just get a pointer to that, and pass it to w.Write, + // avoiding an allocation. + if int(sz) <= m.R.BufferSize() { + var nn int + var buf []byte + buf, err = m.R.Next(int(sz)) + if err != nil { + if err == io.ErrUnexpectedEOF { + err = ErrShortBytes + } + return 0, err + } + nn, err = w.Write(buf) + n += int64(nn) + } else { + // Fall back to io.CopyN. + // May avoid allocating if w is a ReaderFrom (e.g. bytes.Buffer) + n, err = io.CopyN(w, m.R, int64(sz)) + if err == io.ErrUnexpectedEOF { + err = ErrShortBytes + } + } + if err != nil { + return n, err + } else if n < int64(sz) { + return n, io.ErrShortWrite + } + + // for maps and slices, read elements + for x := uintptr(0); x < o; x++ { + var n2 int64 + n2, err = m.CopyNext(w) + if err != nil { + return n, err + } + n += n2 + } + return n, nil +} + +// ReadFull implements `io.ReadFull` +func (m *Reader) ReadFull(p []byte) (int, error) { + return m.R.ReadFull(p) +} + +// Reset resets the underlying reader. +func (m *Reader) Reset(r io.Reader) { m.R.Reset(r) } + +// Buffered returns the number of bytes currently in the read buffer. +func (m *Reader) Buffered() int { return m.R.Buffered() } + +// BufferSize returns the capacity of the read buffer. +func (m *Reader) BufferSize() int { return m.R.BufferSize() } + +// NextType returns the next object type to be decoded. +func (m *Reader) NextType() (Type, error) { + p, err := m.R.Peek(1) + if err != nil { + return InvalidType, err + } + t := getType(p[0]) + if t == InvalidType { + return t, InvalidPrefixError(p[0]) + } + if t == ExtensionType { + v, err := m.peekExtensionType() + if err != nil { + return InvalidType, err + } + switch v { + case Complex64Extension: + return Complex64Type, nil + case Complex128Extension: + return Complex128Type, nil + case TimeExtension: + return TimeType, nil + } + } + return t, nil +} + +// IsNil returns whether or not +// the next byte is a null messagepack byte +func (m *Reader) IsNil() bool { + p, err := m.R.Peek(1) + return err == nil && p[0] == mnil +} + +// getNextSize returns the size of the next object on the wire. +// returns (obj size, obj elements, error) +// only maps and arrays have non-zero obj elements +// for maps and arrays, obj size does not include elements +// +// use uintptr b/c it's guaranteed to be large enough +// to hold whatever we can fit in memory. +func getNextSize(r *fwd.Reader) (uintptr, uintptr, error) { + b, err := r.Peek(1) + if err != nil { + return 0, 0, err + } + lead := b[0] + spec := getBytespec(lead) + size, mode := spec.size, spec.extra + if size == 0 { + return 0, 0, InvalidPrefixError(lead) + } + if mode >= 0 { + return uintptr(size), uintptr(mode), nil + } + b, err = r.Peek(int(size)) + if err != nil { + return 0, 0, err + } + switch mode { + case extra8: + return uintptr(size) + uintptr(b[1]), 0, nil + case extra16: + return uintptr(size) + uintptr(big.Uint16(b[1:])), 0, nil + case extra32: + return uintptr(size) + uintptr(big.Uint32(b[1:])), 0, nil + case map16v: + return uintptr(size), 2 * uintptr(big.Uint16(b[1:])), nil + case map32v: + return uintptr(size), 2 * uintptr(big.Uint32(b[1:])), nil + case array16v: + return uintptr(size), uintptr(big.Uint16(b[1:])), nil + case array32v: + return uintptr(size), uintptr(big.Uint32(b[1:])), nil + default: + return 0, 0, fatal + } +} + +// Skip skips over the next object, regardless of +// its type. If it is an array or map, the whole array +// or map will be skipped. +func (m *Reader) Skip() error { + var ( + v uintptr // bytes + o uintptr // objects + err error + p []byte + ) + + // we can use the faster + // method if we have enough + // buffered data + if m.R.Buffered() >= 5 { + p, err = m.R.Peek(5) + if err != nil { + return err + } + v, o, err = getSize(p) + if err != nil { + return err + } + } else { + v, o, err = getNextSize(m.R) + if err != nil { + return err + } + } + + // 'v' is always non-zero + // if err == nil + _, err = m.R.Skip(int(v)) + if err != nil { + return err + } + + // for maps and slices, skip elements + for x := uintptr(0); x < o; x++ { + err = m.Skip() + if err != nil { + return err + } + } + return nil +} + +// ReadMapHeader reads the next object +// as a map header and returns the size +// of the map and the number of bytes written. +// It will return a TypeError{} if the next +// object is not a map. +func (m *Reader) ReadMapHeader() (sz uint32, err error) { + var p []byte + var lead byte + p, err = m.R.Peek(1) + if err != nil { + return + } + lead = p[0] + if isfixmap(lead) { + sz = uint32(rfixmap(lead)) + _, err = m.R.Skip(1) + return + } + switch lead { + case mmap16: + p, err = m.R.Next(3) + if err != nil { + return + } + sz = uint32(big.Uint16(p[1:])) + return + case mmap32: + p, err = m.R.Next(5) + if err != nil { + return + } + sz = big.Uint32(p[1:]) + return + default: + err = badPrefix(MapType, lead) + return + } +} + +// ReadMapKey reads either a 'str' or 'bin' field from +// the reader and returns the value as a []byte. It uses +// scratch for storage if it is large enough. +func (m *Reader) ReadMapKey(scratch []byte) ([]byte, error) { + out, err := m.ReadStringAsBytes(scratch) + if err != nil { + if tperr, ok := err.(TypeError); ok && tperr.Encoded == BinType { + return m.ReadBytes(scratch) + } + return nil, err + } + return out, nil +} + +// ReadMapKeyPtr returns a []byte pointing to the contents +// of a valid map key. The key cannot be empty, and it +// must be shorter than the total buffer size of the +// *Reader. Additionally, the returned slice is only +// valid until the next *Reader method call. Users +// should exercise extreme care when using this +// method; writing into the returned slice may +// corrupt future reads. +func (m *Reader) ReadMapKeyPtr() ([]byte, error) { + p, err := m.R.Peek(1) + if err != nil { + return nil, err + } + lead := p[0] + var read int + if isfixstr(lead) { + read = int(rfixstr(lead)) + m.R.Skip(1) + goto fill + } + switch lead { + case mstr8, mbin8: + p, err = m.R.Next(2) + if err != nil { + return nil, err + } + read = int(p[1]) + case mstr16, mbin16: + p, err = m.R.Next(3) + if err != nil { + return nil, err + } + read = int(big.Uint16(p[1:])) + case mstr32, mbin32: + p, err = m.R.Next(5) + if err != nil { + return nil, err + } + read = int(big.Uint32(p[1:])) + default: + return nil, badPrefix(StrType, lead) + } +fill: + if read == 0 { + return nil, ErrShortBytes + } + return m.R.Next(read) +} + +// ReadArrayHeader reads the next object as an +// array header and returns the size of the array +// and the number of bytes read. +func (m *Reader) ReadArrayHeader() (sz uint32, err error) { + var lead byte + var p []byte + p, err = m.R.Peek(1) + if err != nil { + return + } + lead = p[0] + if isfixarray(lead) { + sz = uint32(rfixarray(lead)) + _, err = m.R.Skip(1) + return + } + switch lead { + case marray16: + p, err = m.R.Next(3) + if err != nil { + return + } + sz = uint32(big.Uint16(p[1:])) + return + + case marray32: + p, err = m.R.Next(5) + if err != nil { + return + } + sz = big.Uint32(p[1:]) + return + + default: + err = badPrefix(ArrayType, lead) + return + } +} + +// ReadNil reads a 'nil' MessagePack byte from the reader +func (m *Reader) ReadNil() error { + p, err := m.R.Peek(1) + if err != nil { + return err + } + if p[0] != mnil { + return badPrefix(NilType, p[0]) + } + _, err = m.R.Skip(1) + return err +} + +// ReadFloat64 reads a float64 from the reader. +// (If the value on the wire is encoded as a float32, +// it will be up-cast to a float64.) +func (m *Reader) ReadFloat64() (f float64, err error) { + var p []byte + p, err = m.R.Peek(9) + if err != nil { + // we'll allow a coversion from float32 to float64, + // since we don't lose any precision + if err == io.EOF && len(p) > 0 && p[0] == mfloat32 { + ef, err := m.ReadFloat32() + return float64(ef), err + } + return + } + if p[0] != mfloat64 { + // see above + if p[0] == mfloat32 { + ef, err := m.ReadFloat32() + return float64(ef), err + } + err = badPrefix(Float64Type, p[0]) + return + } + f = math.Float64frombits(getMuint64(p)) + _, err = m.R.Skip(9) + return +} + +// ReadFloat32 reads a float32 from the reader +func (m *Reader) ReadFloat32() (f float32, err error) { + var p []byte + p, err = m.R.Peek(5) + if err != nil { + return + } + if p[0] != mfloat32 { + err = badPrefix(Float32Type, p[0]) + return + } + f = math.Float32frombits(getMuint32(p)) + _, err = m.R.Skip(5) + return +} + +// ReadBool reads a bool from the reader +func (m *Reader) ReadBool() (b bool, err error) { + var p []byte + p, err = m.R.Peek(1) + if err != nil { + return + } + switch p[0] { + case mtrue: + b = true + case mfalse: + default: + err = badPrefix(BoolType, p[0]) + return + } + _, err = m.R.Skip(1) + return +} + +// ReadDuration reads a time.Duration from the reader +func (m *Reader) ReadDuration() (d time.Duration, err error) { + i, err := m.ReadInt64() + return time.Duration(i), err +} + +// ReadInt64 reads an int64 from the reader +func (m *Reader) ReadInt64() (i int64, err error) { + var p []byte + var lead byte + p, err = m.R.Peek(1) + if err != nil { + return + } + lead = p[0] + + if isfixint(lead) { + i = int64(rfixint(lead)) + _, err = m.R.Skip(1) + return + } else if isnfixint(lead) { + i = int64(rnfixint(lead)) + _, err = m.R.Skip(1) + return + } + + switch lead { + case mint8: + p, err = m.R.Next(2) + if err != nil { + return + } + i = int64(getMint8(p)) + return + + case muint8: + p, err = m.R.Next(2) + if err != nil { + return + } + i = int64(getMuint8(p)) + return + + case mint16: + p, err = m.R.Next(3) + if err != nil { + return + } + i = int64(getMint16(p)) + return + + case muint16: + p, err = m.R.Next(3) + if err != nil { + return + } + i = int64(getMuint16(p)) + return + + case mint32: + p, err = m.R.Next(5) + if err != nil { + return + } + i = int64(getMint32(p)) + return + + case muint32: + p, err = m.R.Next(5) + if err != nil { + return + } + i = int64(getMuint32(p)) + return + + case mint64: + p, err = m.R.Next(9) + if err != nil { + return + } + i = getMint64(p) + return + + case muint64: + p, err = m.R.Next(9) + if err != nil { + return + } + u := getMuint64(p) + if u > math.MaxInt64 { + err = UintOverflow{Value: u, FailedBitsize: 64} + return + } + i = int64(u) + return + + default: + err = badPrefix(IntType, lead) + return + } +} + +// ReadInt32 reads an int32 from the reader +func (m *Reader) ReadInt32() (i int32, err error) { + var in int64 + in, err = m.ReadInt64() + if in > math.MaxInt32 || in < math.MinInt32 { + err = IntOverflow{Value: in, FailedBitsize: 32} + return + } + i = int32(in) + return +} + +// ReadInt16 reads an int16 from the reader +func (m *Reader) ReadInt16() (i int16, err error) { + var in int64 + in, err = m.ReadInt64() + if in > math.MaxInt16 || in < math.MinInt16 { + err = IntOverflow{Value: in, FailedBitsize: 16} + return + } + i = int16(in) + return +} + +// ReadInt8 reads an int8 from the reader +func (m *Reader) ReadInt8() (i int8, err error) { + var in int64 + in, err = m.ReadInt64() + if in > math.MaxInt8 || in < math.MinInt8 { + err = IntOverflow{Value: in, FailedBitsize: 8} + return + } + i = int8(in) + return +} + +// ReadInt reads an int from the reader +func (m *Reader) ReadInt() (i int, err error) { + if smallint { + var in int32 + in, err = m.ReadInt32() + i = int(in) + return + } + var in int64 + in, err = m.ReadInt64() + i = int(in) + return +} + +// ReadUint64 reads a uint64 from the reader +func (m *Reader) ReadUint64() (u uint64, err error) { + var p []byte + var lead byte + p, err = m.R.Peek(1) + if err != nil { + return + } + lead = p[0] + if isfixint(lead) { + u = uint64(rfixint(lead)) + _, err = m.R.Skip(1) + return + } + switch lead { + case mint8: + p, err = m.R.Next(2) + if err != nil { + return + } + v := int64(getMint8(p)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + return + + case muint8: + p, err = m.R.Next(2) + if err != nil { + return + } + u = uint64(getMuint8(p)) + return + + case mint16: + p, err = m.R.Next(3) + if err != nil { + return + } + v := int64(getMint16(p)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + return + + case muint16: + p, err = m.R.Next(3) + if err != nil { + return + } + u = uint64(getMuint16(p)) + return + + case mint32: + p, err = m.R.Next(5) + if err != nil { + return + } + v := int64(getMint32(p)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + return + + case muint32: + p, err = m.R.Next(5) + if err != nil { + return + } + u = uint64(getMuint32(p)) + return + + case mint64: + p, err = m.R.Next(9) + if err != nil { + return + } + v := int64(getMint64(p)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + return + + case muint64: + p, err = m.R.Next(9) + if err != nil { + return + } + u = getMuint64(p) + return + + default: + if isnfixint(lead) { + err = UintBelowZero{Value: int64(rnfixint(lead))} + } else { + err = badPrefix(UintType, lead) + } + return + + } +} + +// ReadUint32 reads a uint32 from the reader +func (m *Reader) ReadUint32() (u uint32, err error) { + var in uint64 + in, err = m.ReadUint64() + if in > math.MaxUint32 { + err = UintOverflow{Value: in, FailedBitsize: 32} + return + } + u = uint32(in) + return +} + +// ReadUint16 reads a uint16 from the reader +func (m *Reader) ReadUint16() (u uint16, err error) { + var in uint64 + in, err = m.ReadUint64() + if in > math.MaxUint16 { + err = UintOverflow{Value: in, FailedBitsize: 16} + return + } + u = uint16(in) + return +} + +// ReadUint8 reads a uint8 from the reader +func (m *Reader) ReadUint8() (u uint8, err error) { + var in uint64 + in, err = m.ReadUint64() + if in > math.MaxUint8 { + err = UintOverflow{Value: in, FailedBitsize: 8} + return + } + u = uint8(in) + return +} + +// ReadUint reads a uint from the reader +func (m *Reader) ReadUint() (u uint, err error) { + if smallint { + var un uint32 + un, err = m.ReadUint32() + u = uint(un) + return + } + var un uint64 + un, err = m.ReadUint64() + u = uint(un) + return +} + +// ReadByte is analogous to ReadUint8. +// +// NOTE: this is *not* an implementation +// of io.ByteReader. +func (m *Reader) ReadByte() (b byte, err error) { + var in uint64 + in, err = m.ReadUint64() + if in > math.MaxUint8 { + err = UintOverflow{Value: in, FailedBitsize: 8} + return + } + b = byte(in) + return +} + +// ReadBytes reads a MessagePack 'bin' object +// from the reader and returns its value. It may +// use 'scratch' for storage if it is non-nil. +func (m *Reader) ReadBytes(scratch []byte) (b []byte, err error) { + var p []byte + var lead byte + p, err = m.R.Peek(2) + if err != nil { + return + } + lead = p[0] + var read int64 + switch lead { + case mbin8: + read = int64(p[1]) + m.R.Skip(2) + case mbin16: + p, err = m.R.Next(3) + if err != nil { + return + } + read = int64(big.Uint16(p[1:])) + case mbin32: + p, err = m.R.Next(5) + if err != nil { + return + } + read = int64(big.Uint32(p[1:])) + default: + err = badPrefix(BinType, lead) + return + } + if int64(cap(scratch)) < read { + b = make([]byte, read) + } else { + b = scratch[0:read] + } + _, err = m.R.ReadFull(b) + return +} + +// ReadBytesHeader reads the size header +// of a MessagePack 'bin' object. The user +// is responsible for dealing with the next +// 'sz' bytes from the reader in an application-specific +// way. +func (m *Reader) ReadBytesHeader() (sz uint32, err error) { + var p []byte + p, err = m.R.Peek(1) + if err != nil { + return + } + switch p[0] { + case mbin8: + p, err = m.R.Next(2) + if err != nil { + return + } + sz = uint32(p[1]) + return + case mbin16: + p, err = m.R.Next(3) + if err != nil { + return + } + sz = uint32(big.Uint16(p[1:])) + return + case mbin32: + p, err = m.R.Next(5) + if err != nil { + return + } + sz = uint32(big.Uint32(p[1:])) + return + default: + err = badPrefix(BinType, p[0]) + return + } +} + +// ReadExactBytes reads a MessagePack 'bin'-encoded +// object off of the wire into the provided slice. An +// ArrayError will be returned if the object is not +// exactly the length of the input slice. +func (m *Reader) ReadExactBytes(into []byte) error { + p, err := m.R.Peek(2) + if err != nil { + return err + } + lead := p[0] + var read int64 // bytes to read + var skip int // prefix size to skip + switch lead { + case mbin8: + read = int64(p[1]) + skip = 2 + case mbin16: + p, err = m.R.Peek(3) + if err != nil { + return err + } + read = int64(big.Uint16(p[1:])) + skip = 3 + case mbin32: + p, err = m.R.Peek(5) + if err != nil { + return err + } + read = int64(big.Uint32(p[1:])) + skip = 5 + default: + return badPrefix(BinType, lead) + } + if read != int64(len(into)) { + return ArrayError{Wanted: uint32(len(into)), Got: uint32(read)} + } + m.R.Skip(skip) + _, err = m.R.ReadFull(into) + return err +} + +// ReadStringAsBytes reads a MessagePack 'str' (utf-8) string +// and returns its value as bytes. It may use 'scratch' for storage +// if it is non-nil. +func (m *Reader) ReadStringAsBytes(scratch []byte) (b []byte, err error) { + var p []byte + var lead byte + p, err = m.R.Peek(1) + if err != nil { + return + } + lead = p[0] + var read int64 + + if isfixstr(lead) { + read = int64(rfixstr(lead)) + m.R.Skip(1) + goto fill + } + + switch lead { + case mstr8: + p, err = m.R.Next(2) + if err != nil { + return + } + read = int64(uint8(p[1])) + case mstr16: + p, err = m.R.Next(3) + if err != nil { + return + } + read = int64(big.Uint16(p[1:])) + case mstr32: + p, err = m.R.Next(5) + if err != nil { + return + } + read = int64(big.Uint32(p[1:])) + default: + err = badPrefix(StrType, lead) + return + } +fill: + if int64(cap(scratch)) < read { + b = make([]byte, read) + } else { + b = scratch[0:read] + } + _, err = m.R.ReadFull(b) + return +} + +// ReadStringHeader reads a string header +// off of the wire. The user is then responsible +// for dealing with the next 'sz' bytes from +// the reader in an application-specific manner. +func (m *Reader) ReadStringHeader() (sz uint32, err error) { + var p []byte + p, err = m.R.Peek(1) + if err != nil { + return + } + lead := p[0] + if isfixstr(lead) { + sz = uint32(rfixstr(lead)) + m.R.Skip(1) + return + } + switch lead { + case mstr8: + p, err = m.R.Next(2) + if err != nil { + return + } + sz = uint32(p[1]) + return + case mstr16: + p, err = m.R.Next(3) + if err != nil { + return + } + sz = uint32(big.Uint16(p[1:])) + return + case mstr32: + p, err = m.R.Next(5) + if err != nil { + return + } + sz = big.Uint32(p[1:]) + return + default: + err = badPrefix(StrType, lead) + return + } +} + +// ReadString reads a utf-8 string from the reader +func (m *Reader) ReadString() (s string, err error) { + var p []byte + var lead byte + var read int64 + p, err = m.R.Peek(1) + if err != nil { + return + } + lead = p[0] + + if isfixstr(lead) { + read = int64(rfixstr(lead)) + m.R.Skip(1) + goto fill + } + + switch lead { + case mstr8: + p, err = m.R.Next(2) + if err != nil { + return + } + read = int64(uint8(p[1])) + case mstr16: + p, err = m.R.Next(3) + if err != nil { + return + } + read = int64(big.Uint16(p[1:])) + case mstr32: + p, err = m.R.Next(5) + if err != nil { + return + } + read = int64(big.Uint32(p[1:])) + default: + err = badPrefix(StrType, lead) + return + } +fill: + if read == 0 { + s, err = "", nil + return + } + // reading into the memory + // that will become the string + // itself has vastly superior + // worst-case performance, because + // the reader buffer doesn't have + // to be large enough to hold the string. + // the idea here is to make it more + // difficult for someone malicious + // to cause the system to run out of + // memory by sending very large strings. + // + // NOTE: this works because the argument + // passed to (*fwd.Reader).ReadFull escapes + // to the heap; its argument may, in turn, + // be passed to the underlying reader, and + // thus escape analysis *must* conclude that + // 'out' escapes. + out := make([]byte, read) + _, err = m.R.ReadFull(out) + if err != nil { + return + } + s = UnsafeString(out) + return +} + +// ReadComplex64 reads a complex64 from the reader +func (m *Reader) ReadComplex64() (f complex64, err error) { + var p []byte + p, err = m.R.Peek(10) + if err != nil { + return + } + if p[0] != mfixext8 { + err = badPrefix(Complex64Type, p[0]) + return + } + if int8(p[1]) != Complex64Extension { + err = errExt(int8(p[1]), Complex64Extension) + return + } + f = complex(math.Float32frombits(big.Uint32(p[2:])), + math.Float32frombits(big.Uint32(p[6:]))) + _, err = m.R.Skip(10) + return +} + +// ReadComplex128 reads a complex128 from the reader +func (m *Reader) ReadComplex128() (f complex128, err error) { + var p []byte + p, err = m.R.Peek(18) + if err != nil { + return + } + if p[0] != mfixext16 { + err = badPrefix(Complex128Type, p[0]) + return + } + if int8(p[1]) != Complex128Extension { + err = errExt(int8(p[1]), Complex128Extension) + return + } + f = complex(math.Float64frombits(big.Uint64(p[2:])), + math.Float64frombits(big.Uint64(p[10:]))) + _, err = m.R.Skip(18) + return +} + +// ReadMapStrIntf reads a MessagePack map into a map[string]interface{}. +// (You must pass a non-nil map into the function.) +func (m *Reader) ReadMapStrIntf(mp map[string]interface{}) (err error) { + var sz uint32 + sz, err = m.ReadMapHeader() + if err != nil { + return + } + for key := range mp { + delete(mp, key) + } + for i := uint32(0); i < sz; i++ { + var key string + var val interface{} + key, err = m.ReadString() + if err != nil { + return + } + val, err = m.ReadIntf() + if err != nil { + return + } + mp[key] = val + } + return +} + +// ReadTime reads a time.Time object from the reader. +// The returned time's location will be set to time.Local. +func (m *Reader) ReadTime() (t time.Time, err error) { + var p []byte + p, err = m.R.Peek(15) + if err != nil { + return + } + if p[0] != mext8 || p[1] != 12 { + err = badPrefix(TimeType, p[0]) + return + } + if int8(p[2]) != TimeExtension { + err = errExt(int8(p[2]), TimeExtension) + return + } + sec, nsec := getUnix(p[3:]) + t = time.Unix(sec, int64(nsec)).Local() + _, err = m.R.Skip(15) + return +} + +// ReadIntf reads out the next object as a raw interface{}. +// Arrays are decoded as []interface{}, and maps are decoded +// as map[string]interface{}. Integers are decoded as int64 +// and unsigned integers are decoded as uint64. +func (m *Reader) ReadIntf() (i interface{}, err error) { + var t Type + t, err = m.NextType() + if err != nil { + return + } + switch t { + case BoolType: + i, err = m.ReadBool() + return + + case IntType: + i, err = m.ReadInt64() + return + + case UintType: + i, err = m.ReadUint64() + return + + case BinType: + i, err = m.ReadBytes(nil) + return + + case StrType: + i, err = m.ReadString() + return + + case Complex64Type: + i, err = m.ReadComplex64() + return + + case Complex128Type: + i, err = m.ReadComplex128() + return + + case TimeType: + i, err = m.ReadTime() + return + + case DurationType: + i, err = m.ReadDuration() + return + + case ExtensionType: + var t int8 + t, err = m.peekExtensionType() + if err != nil { + return + } + f, ok := extensionReg[t] + if ok { + e := f() + err = m.ReadExtension(e) + i = e + return + } + var e RawExtension + e.Type = t + err = m.ReadExtension(&e) + i = &e + return + + case MapType: + mp := make(map[string]interface{}) + err = m.ReadMapStrIntf(mp) + i = mp + return + + case NilType: + err = m.ReadNil() + i = nil + return + + case Float32Type: + i, err = m.ReadFloat32() + return + + case Float64Type: + i, err = m.ReadFloat64() + return + + case ArrayType: + var sz uint32 + sz, err = m.ReadArrayHeader() + + if err != nil { + return + } + out := make([]interface{}, int(sz)) + for j := range out { + out[j], err = m.ReadIntf() + if err != nil { + return + } + } + i = out + return + + default: + return nil, fatal // unreachable + } +} diff --git a/vendor/github.com/tinylib/msgp/msgp/read_bytes.go b/vendor/github.com/tinylib/msgp/msgp/read_bytes.go new file mode 100644 index 0000000..a204ac4 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/read_bytes.go @@ -0,0 +1,1303 @@ +package msgp + +import ( + "bytes" + "encoding/binary" + "math" + "time" +) + +var big = binary.BigEndian + +// NextType returns the type of the next +// object in the slice. If the length +// of the input is zero, it returns +// [InvalidType]. +func NextType(b []byte) Type { + if len(b) == 0 { + return InvalidType + } + spec := getBytespec(b[0]) + t := spec.typ + if t == ExtensionType && len(b) > int(spec.size) { + var tp int8 + if spec.extra == constsize { + tp = int8(b[1]) + } else { + tp = int8(b[spec.size-1]) + } + switch tp { + case TimeExtension: + return TimeType + case Complex128Extension: + return Complex128Type + case Complex64Extension: + return Complex64Type + default: + return ExtensionType + } + } + return t +} + +// IsNil returns true if len(b)>0 and +// the leading byte is a 'nil' MessagePack +// byte; false otherwise +func IsNil(b []byte) bool { + if len(b) != 0 && b[0] == mnil { + return true + } + return false +} + +// Raw is raw MessagePack. +// Raw allows you to read and write +// data without interpreting its contents. +type Raw []byte + +// MarshalMsg implements [Marshaler]. +// It appends the raw contents of 'raw' +// to the provided byte slice. If 'raw' +// is 0 bytes, 'nil' will be appended instead. +func (r Raw) MarshalMsg(b []byte) ([]byte, error) { + i := len(r) + if i == 0 { + return AppendNil(b), nil + } + o, l := ensure(b, i) + copy(o[l:], []byte(r)) + return o, nil +} + +// UnmarshalMsg implements [Unmarshaler]. +// It sets the contents of *Raw to be the next +// object in the provided byte slice. +func (r *Raw) UnmarshalMsg(b []byte) ([]byte, error) { + l := len(b) + out, err := Skip(b) + if err != nil { + return b, err + } + rlen := l - len(out) + if IsNil(b[:rlen]) { + rlen = 0 + } + if cap(*r) < rlen { + *r = make(Raw, rlen) + } else { + *r = (*r)[0:rlen] + } + copy(*r, b[:rlen]) + return out, nil +} + +// EncodeMsg implements [Encodable]. +// It writes the raw bytes to the writer. +// If r is empty, it writes 'nil' instead. +func (r Raw) EncodeMsg(w *Writer) error { + if len(r) == 0 { + return w.WriteNil() + } + _, err := w.Write([]byte(r)) + return err +} + +// DecodeMsg implements [Decodable]. +// It sets the value of *Raw to be the +// next object on the wire. +func (r *Raw) DecodeMsg(f *Reader) error { + *r = (*r)[:0] + err := appendNext(f, (*[]byte)(r)) + if IsNil(*r) { + *r = (*r)[:0] + } + return err +} + +// Msgsize implements [Sizer]. +func (r Raw) Msgsize() int { + l := len(r) + if l == 0 { + return 1 // for 'nil' + } + return l +} + +func appendNext(f *Reader, d *[]byte) error { + amt, o, err := getNextSize(f.R) + if err != nil { + return err + } + var i int + *d, i = ensure(*d, int(amt)) + _, err = f.R.ReadFull((*d)[i:]) + if err != nil { + return err + } + for o > 0 { + err = appendNext(f, d) + if err != nil { + return err + } + o-- + } + return nil +} + +// MarshalJSON implements [json.Marshaler]. +func (r *Raw) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + _, err := UnmarshalAsJSON(&buf, []byte(*r)) + return buf.Bytes(), err +} + +// ReadMapHeaderBytes reads a map header size +// from 'b' and returns the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a map) +func ReadMapHeaderBytes(b []byte) (sz uint32, o []byte, err error) { + l := len(b) + if l < 1 { + err = ErrShortBytes + return + } + + lead := b[0] + if isfixmap(lead) { + sz = uint32(rfixmap(lead)) + o = b[1:] + return + } + + switch lead { + case mmap16: + if l < 3 { + err = ErrShortBytes + return + } + sz = uint32(big.Uint16(b[1:])) + o = b[3:] + return + + case mmap32: + if l < 5 { + err = ErrShortBytes + return + } + sz = big.Uint32(b[1:]) + o = b[5:] + return + + default: + err = badPrefix(MapType, lead) + return + } +} + +// ReadMapKeyZC attempts to read a map key +// from 'b' and returns the key bytes and the remaining bytes +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a str or bin) +func ReadMapKeyZC(b []byte) ([]byte, []byte, error) { + o, x, err := ReadStringZC(b) + if err != nil { + if tperr, ok := err.(TypeError); ok && tperr.Encoded == BinType { + return ReadBytesZC(b) + } + return nil, b, err + } + return o, x, nil +} + +// ReadArrayHeaderBytes attempts to read +// the array header size off of 'b' and return +// the size and remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not an array) +func ReadArrayHeaderBytes(b []byte) (sz uint32, o []byte, err error) { + if len(b) < 1 { + return 0, nil, ErrShortBytes + } + lead := b[0] + if isfixarray(lead) { + sz = uint32(rfixarray(lead)) + o = b[1:] + return + } + + switch lead { + case marray16: + if len(b) < 3 { + err = ErrShortBytes + return + } + sz = uint32(big.Uint16(b[1:])) + o = b[3:] + return + + case marray32: + if len(b) < 5 { + err = ErrShortBytes + return + } + sz = big.Uint32(b[1:]) + o = b[5:] + return + + default: + err = badPrefix(ArrayType, lead) + return + } +} + +// ReadBytesHeader reads the 'bin' header size +// off of 'b' and returns the size and remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a bin object) +func ReadBytesHeader(b []byte) (sz uint32, o []byte, err error) { + if len(b) < 1 { + return 0, nil, ErrShortBytes + } + switch b[0] { + case mbin8: + if len(b) < 2 { + err = ErrShortBytes + return + } + sz = uint32(b[1]) + o = b[2:] + return + case mbin16: + if len(b) < 3 { + err = ErrShortBytes + return + } + sz = uint32(big.Uint16(b[1:])) + o = b[3:] + return + case mbin32: + if len(b) < 5 { + err = ErrShortBytes + return + } + sz = big.Uint32(b[1:]) + o = b[5:] + return + default: + err = badPrefix(BinType, b[0]) + return + } +} + +// ReadNilBytes tries to read a "nil" byte +// off of 'b' and return the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a 'nil') +// - [InvalidPrefixError] +func ReadNilBytes(b []byte) ([]byte, error) { + if len(b) < 1 { + return nil, ErrShortBytes + } + if b[0] != mnil { + return b, badPrefix(NilType, b[0]) + } + return b[1:], nil +} + +// ReadFloat64Bytes tries to read a float64 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a float64) +func ReadFloat64Bytes(b []byte) (f float64, o []byte, err error) { + if len(b) < 9 { + if len(b) >= 5 && b[0] == mfloat32 { + var tf float32 + tf, o, err = ReadFloat32Bytes(b) + f = float64(tf) + return + } + err = ErrShortBytes + return + } + + if b[0] != mfloat64 { + if b[0] == mfloat32 { + var tf float32 + tf, o, err = ReadFloat32Bytes(b) + f = float64(tf) + return + } + err = badPrefix(Float64Type, b[0]) + return + } + + f = math.Float64frombits(getMuint64(b)) + o = b[9:] + return +} + +// ReadFloat32Bytes tries to read a float64 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a float32) +func ReadFloat32Bytes(b []byte) (f float32, o []byte, err error) { + if len(b) < 5 { + err = ErrShortBytes + return + } + + if b[0] != mfloat32 { + err = TypeError{Method: Float32Type, Encoded: getType(b[0])} + return + } + + f = math.Float32frombits(getMuint32(b)) + o = b[5:] + return +} + +// ReadBoolBytes tries to read a float64 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a bool) +func ReadBoolBytes(b []byte) (bool, []byte, error) { + if len(b) < 1 { + return false, b, ErrShortBytes + } + switch b[0] { + case mtrue: + return true, b[1:], nil + case mfalse: + return false, b[1:], nil + default: + return false, b, badPrefix(BoolType, b[0]) + } +} + +// ReadDurationBytes tries to read a time.Duration +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - TypeError (not a int) +func ReadDurationBytes(b []byte) (d time.Duration, o []byte, err error) { + i, o, err := ReadInt64Bytes(b) + return time.Duration(i), o, err +} + +// ReadInt64Bytes tries to read an int64 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a int) +func ReadInt64Bytes(b []byte) (i int64, o []byte, err error) { + l := len(b) + if l < 1 { + return 0, nil, ErrShortBytes + } + + lead := b[0] + if isfixint(lead) { + i = int64(rfixint(lead)) + o = b[1:] + return + } + if isnfixint(lead) { + i = int64(rnfixint(lead)) + o = b[1:] + return + } + + switch lead { + case mint8: + if l < 2 { + err = ErrShortBytes + return + } + i = int64(getMint8(b)) + o = b[2:] + return + + case muint8: + if l < 2 { + err = ErrShortBytes + return + } + i = int64(getMuint8(b)) + o = b[2:] + return + + case mint16: + if l < 3 { + err = ErrShortBytes + return + } + i = int64(getMint16(b)) + o = b[3:] + return + + case muint16: + if l < 3 { + err = ErrShortBytes + return + } + i = int64(getMuint16(b)) + o = b[3:] + return + + case mint32: + if l < 5 { + err = ErrShortBytes + return + } + i = int64(getMint32(b)) + o = b[5:] + return + + case muint32: + if l < 5 { + err = ErrShortBytes + return + } + i = int64(getMuint32(b)) + o = b[5:] + return + + case mint64: + if l < 9 { + err = ErrShortBytes + return + } + i = int64(getMint64(b)) + o = b[9:] + return + + case muint64: + if l < 9 { + err = ErrShortBytes + return + } + u := getMuint64(b) + if u > math.MaxInt64 { + err = UintOverflow{Value: u, FailedBitsize: 64} + return + } + i = int64(u) + o = b[9:] + return + + default: + err = badPrefix(IntType, lead) + return + } +} + +// ReadInt32Bytes tries to read an int32 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a int) +// - [IntOverflow] (value doesn't fit in int32) +func ReadInt32Bytes(b []byte) (int32, []byte, error) { + i, o, err := ReadInt64Bytes(b) + if i > math.MaxInt32 || i < math.MinInt32 { + return 0, o, IntOverflow{Value: i, FailedBitsize: 32} + } + return int32(i), o, err +} + +// ReadInt16Bytes tries to read an int16 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a int) +// - [IntOverflow] (value doesn't fit in int16) +func ReadInt16Bytes(b []byte) (int16, []byte, error) { + i, o, err := ReadInt64Bytes(b) + if i > math.MaxInt16 || i < math.MinInt16 { + return 0, o, IntOverflow{Value: i, FailedBitsize: 16} + } + return int16(i), o, err +} + +// ReadInt8Bytes tries to read an int16 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a int) +// - [IntOverflow] (value doesn't fit in int8) +func ReadInt8Bytes(b []byte) (int8, []byte, error) { + i, o, err := ReadInt64Bytes(b) + if i > math.MaxInt8 || i < math.MinInt8 { + return 0, o, IntOverflow{Value: i, FailedBitsize: 8} + } + return int8(i), o, err +} + +// ReadIntBytes tries to read an int +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a int) +// - [IntOverflow] (value doesn't fit in int; 32-bit platforms only) +func ReadIntBytes(b []byte) (int, []byte, error) { + if smallint { + i, b, err := ReadInt32Bytes(b) + return int(i), b, err + } + i, b, err := ReadInt64Bytes(b) + return int(i), b, err +} + +// ReadUint64Bytes tries to read a uint64 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a uint) +func ReadUint64Bytes(b []byte) (u uint64, o []byte, err error) { + l := len(b) + if l < 1 { + return 0, nil, ErrShortBytes + } + + lead := b[0] + if isfixint(lead) { + u = uint64(rfixint(lead)) + o = b[1:] + return + } + + switch lead { + case mint8: + if l < 2 { + err = ErrShortBytes + return + } + v := int64(getMint8(b)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + o = b[2:] + return + + case muint8: + if l < 2 { + err = ErrShortBytes + return + } + u = uint64(getMuint8(b)) + o = b[2:] + return + + case mint16: + if l < 3 { + err = ErrShortBytes + return + } + v := int64(getMint16(b)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + o = b[3:] + return + + case muint16: + if l < 3 { + err = ErrShortBytes + return + } + u = uint64(getMuint16(b)) + o = b[3:] + return + + case mint32: + if l < 5 { + err = ErrShortBytes + return + } + v := int64(getMint32(b)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + o = b[5:] + return + + case muint32: + if l < 5 { + err = ErrShortBytes + return + } + u = uint64(getMuint32(b)) + o = b[5:] + return + + case mint64: + if l < 9 { + err = ErrShortBytes + return + } + v := int64(getMint64(b)) + if v < 0 { + err = UintBelowZero{Value: v} + return + } + u = uint64(v) + o = b[9:] + return + + case muint64: + if l < 9 { + err = ErrShortBytes + return + } + u = getMuint64(b) + o = b[9:] + return + + default: + if isnfixint(lead) { + err = UintBelowZero{Value: int64(rnfixint(lead))} + } else { + err = badPrefix(UintType, lead) + } + return + } +} + +// ReadUint32Bytes tries to read a uint32 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a uint) +// - [UintOverflow] (value too large for uint32) +func ReadUint32Bytes(b []byte) (uint32, []byte, error) { + v, o, err := ReadUint64Bytes(b) + if v > math.MaxUint32 { + return 0, nil, UintOverflow{Value: v, FailedBitsize: 32} + } + return uint32(v), o, err +} + +// ReadUint16Bytes tries to read a uint16 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a uint) +// - [UintOverflow] (value too large for uint16) +func ReadUint16Bytes(b []byte) (uint16, []byte, error) { + v, o, err := ReadUint64Bytes(b) + if v > math.MaxUint16 { + return 0, nil, UintOverflow{Value: v, FailedBitsize: 16} + } + return uint16(v), o, err +} + +// ReadUint8Bytes tries to read a uint8 +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a uint) +// - [UintOverflow] (value too large for uint8) +func ReadUint8Bytes(b []byte) (uint8, []byte, error) { + v, o, err := ReadUint64Bytes(b) + if v > math.MaxUint8 { + return 0, nil, UintOverflow{Value: v, FailedBitsize: 8} + } + return uint8(v), o, err +} + +// ReadUintBytes tries to read a uint +// from 'b' and return the value and the remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a uint) +// - [UintOverflow] (value too large for uint; 32-bit platforms only) +func ReadUintBytes(b []byte) (uint, []byte, error) { + if smallint { + u, b, err := ReadUint32Bytes(b) + return uint(u), b, err + } + u, b, err := ReadUint64Bytes(b) + return uint(u), b, err +} + +// ReadByteBytes is analogous to ReadUint8Bytes +func ReadByteBytes(b []byte) (byte, []byte, error) { + return ReadUint8Bytes(b) +} + +// ReadBytesBytes reads a 'bin' object +// from 'b' and returns its vaue and +// the remaining bytes in 'b'. +// +// Possible errors: +// +// - [ErrShortBytes] (too few bytes) +// - [TypeError] (not a 'bin' object) +func ReadBytesBytes(b []byte, scratch []byte) (v []byte, o []byte, err error) { + return readBytesBytes(b, scratch, false) +} + +func readBytesBytes(b []byte, scratch []byte, zc bool) (v []byte, o []byte, err error) { + l := len(b) + if l < 1 { + return nil, nil, ErrShortBytes + } + + lead := b[0] + var read int + switch lead { + case mbin8: + if l < 2 { + err = ErrShortBytes + return + } + + read = int(b[1]) + b = b[2:] + + case mbin16: + if l < 3 { + err = ErrShortBytes + return + } + read = int(big.Uint16(b[1:])) + b = b[3:] + + case mbin32: + if l < 5 { + err = ErrShortBytes + return + } + read = int(big.Uint32(b[1:])) + b = b[5:] + + default: + err = badPrefix(BinType, lead) + return + } + + if len(b) < read { + err = ErrShortBytes + return + } + + // zero-copy + if zc { + v = b[0:read] + o = b[read:] + return + } + + if cap(scratch) >= read { + v = scratch[0:read] + } else { + v = make([]byte, read) + } + + o = b[copy(v, b):] + return +} + +// ReadBytesZC extracts the messagepack-encoded +// binary field without copying. The returned []byte +// points to the same memory as the input slice. +// +// Possible errors: +// +// - [ErrShortBytes] (b not long enough) +// - [TypeError] (object not 'bin') +func ReadBytesZC(b []byte) (v []byte, o []byte, err error) { + return readBytesBytes(b, nil, true) +} + +func ReadExactBytes(b []byte, into []byte) (o []byte, err error) { + l := len(b) + if l < 1 { + err = ErrShortBytes + return + } + + lead := b[0] + var read uint32 + var skip int + switch lead { + case mbin8: + if l < 2 { + err = ErrShortBytes + return + } + + read = uint32(b[1]) + skip = 2 + + case mbin16: + if l < 3 { + err = ErrShortBytes + return + } + read = uint32(big.Uint16(b[1:])) + skip = 3 + + case mbin32: + if l < 5 { + err = ErrShortBytes + return + } + read = uint32(big.Uint32(b[1:])) + skip = 5 + + default: + err = badPrefix(BinType, lead) + return + } + + if read != uint32(len(into)) { + err = ArrayError{Wanted: uint32(len(into)), Got: read} + return + } + + o = b[skip+copy(into, b[skip:]):] + return +} + +// ReadStringZC reads a messagepack string field +// without copying. The returned []byte points +// to the same memory as the input slice. +// +// Possible errors: +// +// - [ErrShortBytes] (b not long enough) +// - [TypeError] (object not 'str') +func ReadStringZC(b []byte) (v []byte, o []byte, err error) { + l := len(b) + if l < 1 { + return nil, nil, ErrShortBytes + } + + lead := b[0] + var read int + + if isfixstr(lead) { + read = int(rfixstr(lead)) + b = b[1:] + } else { + switch lead { + case mstr8: + if l < 2 { + err = ErrShortBytes + return + } + read = int(b[1]) + b = b[2:] + + case mstr16: + if l < 3 { + err = ErrShortBytes + return + } + read = int(big.Uint16(b[1:])) + b = b[3:] + + case mstr32: + if l < 5 { + err = ErrShortBytes + return + } + read = int(big.Uint32(b[1:])) + b = b[5:] + + default: + err = TypeError{Method: StrType, Encoded: getType(lead)} + return + } + } + + if len(b) < read { + err = ErrShortBytes + return + } + + v = b[0:read] + o = b[read:] + return +} + +// ReadStringBytes reads a 'str' object +// from 'b' and returns its value and the +// remaining bytes in 'b'. +// +// Possible errors: +// +// - [ErrShortBytes] (b not long enough) +// - [TypeError] (not 'str' type) +// - [InvalidPrefixError] +func ReadStringBytes(b []byte) (string, []byte, error) { + v, o, err := ReadStringZC(b) + return string(v), o, err +} + +// ReadStringAsBytes reads a 'str' object +// into a slice of bytes. 'v' is the value of +// the 'str' object, which may reside in memory +// pointed to by 'scratch.' 'o' is the remaining bytes +// in 'b'. +// +// Possible errors: +// +// - [ErrShortBytes] (b not long enough) +// - [TypeError] (not 'str' type) +// - [InvalidPrefixError] (unknown type marker) +func ReadStringAsBytes(b []byte, scratch []byte) (v []byte, o []byte, err error) { + var tmp []byte + tmp, o, err = ReadStringZC(b) + v = append(scratch[:0], tmp...) + return +} + +// ReadComplex128Bytes reads a complex128 +// extension object from 'b' and returns the +// remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (not enough bytes in 'b') +// - [TypeError] (object not a complex128) +// - [InvalidPrefixError] +// - [ExtensionTypeError] (object an extension of the correct size, but not a complex128) +func ReadComplex128Bytes(b []byte) (c complex128, o []byte, err error) { + if len(b) < 18 { + err = ErrShortBytes + return + } + if b[0] != mfixext16 { + err = badPrefix(Complex128Type, b[0]) + return + } + if int8(b[1]) != Complex128Extension { + err = errExt(int8(b[1]), Complex128Extension) + return + } + c = complex(math.Float64frombits(big.Uint64(b[2:])), + math.Float64frombits(big.Uint64(b[10:]))) + o = b[18:] + return +} + +// ReadComplex64Bytes reads a complex64 +// extension object from 'b' and returns the +// remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (not enough bytes in 'b') +// - [TypeError] (object not a complex64) +// - [ExtensionTypeError] (object an extension of the correct size, but not a complex64) +func ReadComplex64Bytes(b []byte) (c complex64, o []byte, err error) { + if len(b) < 10 { + err = ErrShortBytes + return + } + if b[0] != mfixext8 { + err = badPrefix(Complex64Type, b[0]) + return + } + if b[1] != Complex64Extension { + err = errExt(int8(b[1]), Complex64Extension) + return + } + c = complex(math.Float32frombits(big.Uint32(b[2:])), + math.Float32frombits(big.Uint32(b[6:]))) + o = b[10:] + return +} + +// ReadTimeBytes reads a time.Time +// extension object from 'b' and returns the +// remaining bytes. +// +// Possible errors: +// +// - [ErrShortBytes] (not enough bytes in 'b') +// - [TypeError] (object not a complex64) +// - [ExtensionTypeError] (object an extension of the correct size, but not a time.Time) +func ReadTimeBytes(b []byte) (t time.Time, o []byte, err error) { + if len(b) < 15 { + err = ErrShortBytes + return + } + if b[0] != mext8 || b[1] != 12 { + err = badPrefix(TimeType, b[0]) + return + } + if int8(b[2]) != TimeExtension { + err = errExt(int8(b[2]), TimeExtension) + return + } + sec, nsec := getUnix(b[3:]) + t = time.Unix(sec, int64(nsec)).Local() + o = b[15:] + return +} + +// ReadMapStrIntfBytes reads a map[string]interface{} +// out of 'b' and returns the map and remaining bytes. +// If 'old' is non-nil, the values will be read into that map. +func ReadMapStrIntfBytes(b []byte, old map[string]interface{}) (v map[string]interface{}, o []byte, err error) { + var sz uint32 + o = b + sz, o, err = ReadMapHeaderBytes(o) + + if err != nil { + return + } + + if old != nil { + for key := range old { + delete(old, key) + } + v = old + } else { + v = make(map[string]interface{}, int(sz)) + } + + for z := uint32(0); z < sz; z++ { + if len(o) < 1 { + err = ErrShortBytes + return + } + var key []byte + key, o, err = ReadMapKeyZC(o) + if err != nil { + return + } + var val interface{} + val, o, err = ReadIntfBytes(o) + if err != nil { + return + } + v[string(key)] = val + } + return +} + +// ReadIntfBytes attempts to read +// the next object out of 'b' as a raw interface{} and +// return the remaining bytes. +func ReadIntfBytes(b []byte) (i interface{}, o []byte, err error) { + if len(b) < 1 { + err = ErrShortBytes + return + } + + k := NextType(b) + + switch k { + case MapType: + i, o, err = ReadMapStrIntfBytes(b, nil) + return + + case ArrayType: + var sz uint32 + sz, o, err = ReadArrayHeaderBytes(b) + if err != nil { + return + } + j := make([]interface{}, int(sz)) + i = j + for d := range j { + j[d], o, err = ReadIntfBytes(o) + if err != nil { + return + } + } + return + + case Float32Type: + i, o, err = ReadFloat32Bytes(b) + return + + case Float64Type: + i, o, err = ReadFloat64Bytes(b) + return + + case IntType: + i, o, err = ReadInt64Bytes(b) + return + + case UintType: + i, o, err = ReadUint64Bytes(b) + return + + case BoolType: + i, o, err = ReadBoolBytes(b) + return + + case TimeType: + i, o, err = ReadTimeBytes(b) + return + + case Complex64Type: + i, o, err = ReadComplex64Bytes(b) + return + + case Complex128Type: + i, o, err = ReadComplex128Bytes(b) + return + + case ExtensionType: + var t int8 + t, err = peekExtension(b) + if err != nil { + return + } + // use a user-defined extension, + // if it's been registered + f, ok := extensionReg[t] + if ok { + e := f() + o, err = ReadExtensionBytes(b, e) + i = e + return + } + // last resort is a raw extension + e := RawExtension{} + e.Type = int8(t) + o, err = ReadExtensionBytes(b, &e) + i = &e + return + + case NilType: + o, err = ReadNilBytes(b) + return + + case BinType: + i, o, err = ReadBytesBytes(b, nil) + return + + case StrType: + i, o, err = ReadStringBytes(b) + return + + default: + err = InvalidPrefixError(b[0]) + return + } +} + +// Skip skips the next object in 'b' and +// returns the remaining bytes. If the object +// is a map or array, all of its elements +// will be skipped. +// +// Possible errors: +// +// - [ErrShortBytes] (not enough bytes in b) +// - [InvalidPrefixError] (bad encoding) +func Skip(b []byte) ([]byte, error) { + sz, asz, err := getSize(b) + if err != nil { + return b, err + } + if uintptr(len(b)) < sz { + return b, ErrShortBytes + } + b = b[sz:] + for asz > 0 { + b, err = Skip(b) + if err != nil { + return b, err + } + asz-- + } + return b, nil +} + +// returns (skip N bytes, skip M objects, error) +func getSize(b []byte) (uintptr, uintptr, error) { + l := len(b) + if l == 0 { + return 0, 0, ErrShortBytes + } + lead := b[0] + spec := getBytespec(lead) // get type information + size, mode := spec.size, spec.extra + if size == 0 { + return 0, 0, InvalidPrefixError(lead) + } + if mode >= 0 { // fixed composites + return uintptr(size), uintptr(mode), nil + } + if l < int(size) { + return 0, 0, ErrShortBytes + } + switch mode { + case extra8: + return uintptr(size) + uintptr(b[1]), 0, nil + case extra16: + return uintptr(size) + uintptr(big.Uint16(b[1:])), 0, nil + case extra32: + return uintptr(size) + uintptr(big.Uint32(b[1:])), 0, nil + case map16v: + return uintptr(size), 2 * uintptr(big.Uint16(b[1:])), nil + case map32v: + return uintptr(size), 2 * uintptr(big.Uint32(b[1:])), nil + case array16v: + return uintptr(size), uintptr(big.Uint16(b[1:])), nil + case array32v: + return uintptr(size), uintptr(big.Uint32(b[1:])), nil + default: + return 0, 0, fatal + } +} diff --git a/vendor/github.com/tinylib/msgp/msgp/size.go b/vendor/github.com/tinylib/msgp/msgp/size.go new file mode 100644 index 0000000..e3a613b --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/size.go @@ -0,0 +1,39 @@ +package msgp + +// The sizes provided +// are the worst-case +// encoded sizes for +// each type. For variable- +// length types ([]byte, string), +// the total encoded size is +// the prefix size plus the +// length of the object. +const ( + Int64Size = 9 + IntSize = Int64Size + UintSize = Int64Size + Int8Size = 2 + Int16Size = 3 + Int32Size = 5 + Uint8Size = 2 + ByteSize = Uint8Size + Uint16Size = 3 + Uint32Size = 5 + Uint64Size = Int64Size + Float64Size = 9 + Float32Size = 5 + Complex64Size = 10 + Complex128Size = 18 + + DurationSize = Int64Size + TimeSize = 15 + BoolSize = 1 + NilSize = 1 + + MapHeaderSize = 5 + ArrayHeaderSize = 5 + + BytesPrefixSize = 5 + StringPrefixSize = 5 + ExtensionPrefixSize = 6 +) diff --git a/vendor/github.com/tinylib/msgp/msgp/unsafe.go b/vendor/github.com/tinylib/msgp/msgp/unsafe.go new file mode 100644 index 0000000..06e8d84 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/unsafe.go @@ -0,0 +1,37 @@ +//go:build !purego && !appengine +// +build !purego,!appengine + +package msgp + +import ( + "unsafe" +) + +// NOTE: +// all of the definition in this file +// should be repeated in appengine.go, +// but without using unsafe + +const ( + // spec says int and uint are always + // the same size, but that int/uint + // size may not be machine word size + smallint = unsafe.Sizeof(int(0)) == 4 +) + +// UnsafeString returns the byte slice as a volatile string +// THIS SHOULD ONLY BE USED BY THE CODE GENERATOR. +// THIS IS EVIL CODE. +// YOU HAVE BEEN WARNED. +func UnsafeString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +// UnsafeBytes returns the string as a byte slice +// +// Deprecated: +// Since this code is no longer used by the code generator, +// UnsafeBytes(s) is precisely equivalent to []byte(s) +func UnsafeBytes(s string) []byte { + return []byte(s) +} diff --git a/vendor/github.com/tinylib/msgp/msgp/write.go b/vendor/github.com/tinylib/msgp/msgp/write.go new file mode 100644 index 0000000..ec2f6f5 --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/write.go @@ -0,0 +1,813 @@ +package msgp + +import ( + "errors" + "io" + "math" + "reflect" + "sync" + "time" +) + +const ( + // min buffer size for the writer + minWriterSize = 18 +) + +// Sizer is an interface implemented +// by types that can estimate their +// size when MessagePack encoded. +// This interface is optional, but +// encoding/marshaling implementations +// may use this as a way to pre-allocate +// memory for serialization. +type Sizer interface { + Msgsize() int +} + +var ( + // Nowhere is an io.Writer to nowhere + Nowhere io.Writer = nwhere{} + + btsType = reflect.TypeOf(([]byte)(nil)) + writerPool = sync.Pool{ + New: func() interface{} { + return &Writer{buf: make([]byte, 2048)} + }, + } +) + +func popWriter(w io.Writer) *Writer { + wr := writerPool.Get().(*Writer) + wr.Reset(w) + return wr +} + +func pushWriter(wr *Writer) { + wr.w = nil + wr.wloc = 0 + writerPool.Put(wr) +} + +// freeW frees a writer for use +// by other processes. It is not necessary +// to call freeW on a writer. However, maintaining +// a reference to a *Writer after calling freeW on +// it will cause undefined behavior. +func freeW(w *Writer) { pushWriter(w) } + +// Require ensures that cap(old)-len(old) >= extra. +func Require(old []byte, extra int) []byte { + l := len(old) + c := cap(old) + r := l + extra + if c >= r { + return old + } else if l == 0 { + return make([]byte, 0, extra) + } + // the new size is the greater + // of double the old capacity + // and the sum of the old length + // and the number of new bytes + // necessary. + c <<= 1 + if c < r { + c = r + } + n := make([]byte, l, c) + copy(n, old) + return n +} + +// nowhere writer +type nwhere struct{} + +func (n nwhere) Write(p []byte) (int, error) { return len(p), nil } + +// Marshaler is the interface implemented +// by types that know how to marshal themselves +// as MessagePack. MarshalMsg appends the marshalled +// form of the object to the provided +// byte slice, returning the extended +// slice and any errors encountered. +type Marshaler interface { + MarshalMsg([]byte) ([]byte, error) +} + +// Encodable is the interface implemented +// by types that know how to write themselves +// as MessagePack using a *msgp.Writer. +type Encodable interface { + EncodeMsg(*Writer) error +} + +// Writer is a buffered writer +// that can be used to write +// MessagePack objects to an io.Writer. +// You must call *Writer.Flush() in order +// to flush all of the buffered data +// to the underlying writer. +type Writer struct { + w io.Writer + buf []byte + wloc int +} + +// NewWriter returns a new *Writer. +func NewWriter(w io.Writer) *Writer { + if wr, ok := w.(*Writer); ok { + return wr + } + return popWriter(w) +} + +// NewWriterSize returns a writer with a custom buffer size. +func NewWriterSize(w io.Writer, sz int) *Writer { + // we must be able to require() 'minWriterSize' + // contiguous bytes, so that is the + // practical minimum buffer size + if sz < minWriterSize { + sz = minWriterSize + } + buf := make([]byte, sz) + return NewWriterBuf(w, buf) +} + +// NewWriterBuf returns a writer with a provided buffer. +// 'buf' is not used when the capacity is smaller than 18, +// custom buffer is allocated instead. +func NewWriterBuf(w io.Writer, buf []byte) *Writer { + if cap(buf) < minWriterSize { + buf = make([]byte, minWriterSize) + } + buf = buf[:cap(buf)] + return &Writer{ + w: w, + buf: buf, + } +} + +// Encode encodes an Encodable to an io.Writer. +func Encode(w io.Writer, e Encodable) error { + wr := NewWriter(w) + err := e.EncodeMsg(wr) + if err == nil { + err = wr.Flush() + } + freeW(wr) + return err +} + +func (mw *Writer) flush() error { + if mw.wloc == 0 { + return nil + } + n, err := mw.w.Write(mw.buf[:mw.wloc]) + if err != nil { + if n > 0 { + mw.wloc = copy(mw.buf, mw.buf[n:mw.wloc]) + } + return err + } + mw.wloc = 0 + return nil +} + +// Flush flushes all of the buffered +// data to the underlying writer. +func (mw *Writer) Flush() error { return mw.flush() } + +// Buffered returns the number bytes in the write buffer +func (mw *Writer) Buffered() int { return len(mw.buf) - mw.wloc } + +func (mw *Writer) avail() int { return len(mw.buf) - mw.wloc } + +func (mw *Writer) bufsize() int { return len(mw.buf) } + +// NOTE: this should only be called with +// a number that is guaranteed to be less than +// len(mw.buf). typically, it is called with a constant. +// +// NOTE: this is a hot code path +func (mw *Writer) require(n int) (int, error) { + c := len(mw.buf) + wl := mw.wloc + if c-wl < n { + if err := mw.flush(); err != nil { + return 0, err + } + wl = mw.wloc + } + mw.wloc += n + return wl, nil +} + +func (mw *Writer) Append(b ...byte) error { + if mw.avail() < len(b) { + err := mw.flush() + if err != nil { + return err + } + } + mw.wloc += copy(mw.buf[mw.wloc:], b) + return nil +} + +// push one byte onto the buffer +// +// NOTE: this is a hot code path +func (mw *Writer) push(b byte) error { + if mw.wloc == len(mw.buf) { + if err := mw.flush(); err != nil { + return err + } + } + mw.buf[mw.wloc] = b + mw.wloc++ + return nil +} + +func (mw *Writer) prefix8(b byte, u uint8) error { + const need = 2 + if len(mw.buf)-mw.wloc < need { + if err := mw.flush(); err != nil { + return err + } + } + prefixu8(mw.buf[mw.wloc:], b, u) + mw.wloc += need + return nil +} + +func (mw *Writer) prefix16(b byte, u uint16) error { + const need = 3 + if len(mw.buf)-mw.wloc < need { + if err := mw.flush(); err != nil { + return err + } + } + prefixu16(mw.buf[mw.wloc:], b, u) + mw.wloc += need + return nil +} + +func (mw *Writer) prefix32(b byte, u uint32) error { + const need = 5 + if len(mw.buf)-mw.wloc < need { + if err := mw.flush(); err != nil { + return err + } + } + prefixu32(mw.buf[mw.wloc:], b, u) + mw.wloc += need + return nil +} + +func (mw *Writer) prefix64(b byte, u uint64) error { + const need = 9 + if len(mw.buf)-mw.wloc < need { + if err := mw.flush(); err != nil { + return err + } + } + prefixu64(mw.buf[mw.wloc:], b, u) + mw.wloc += need + return nil +} + +// Write implements io.Writer, and writes +// data directly to the buffer. +func (mw *Writer) Write(p []byte) (int, error) { + l := len(p) + if mw.avail() < l { + if err := mw.flush(); err != nil { + return 0, err + } + if l > len(mw.buf) { + return mw.w.Write(p) + } + } + mw.wloc += copy(mw.buf[mw.wloc:], p) + return l, nil +} + +// implements io.WriteString +func (mw *Writer) writeString(s string) error { + l := len(s) + if mw.avail() < l { + if err := mw.flush(); err != nil { + return err + } + if l > len(mw.buf) { + _, err := io.WriteString(mw.w, s) + return err + } + } + mw.wloc += copy(mw.buf[mw.wloc:], s) + return nil +} + +// Reset changes the underlying writer used by the Writer +func (mw *Writer) Reset(w io.Writer) { + mw.buf = mw.buf[:cap(mw.buf)] + mw.w = w + mw.wloc = 0 +} + +// WriteMapHeader writes a map header of the given +// size to the writer +func (mw *Writer) WriteMapHeader(sz uint32) error { + switch { + case sz <= 15: + return mw.push(wfixmap(uint8(sz))) + case sz <= math.MaxUint16: + return mw.prefix16(mmap16, uint16(sz)) + default: + return mw.prefix32(mmap32, sz) + } +} + +// WriteArrayHeader writes an array header of the +// given size to the writer +func (mw *Writer) WriteArrayHeader(sz uint32) error { + switch { + case sz <= 15: + return mw.push(wfixarray(uint8(sz))) + case sz <= math.MaxUint16: + return mw.prefix16(marray16, uint16(sz)) + default: + return mw.prefix32(marray32, sz) + } +} + +// WriteNil writes a nil byte to the buffer +func (mw *Writer) WriteNil() error { + return mw.push(mnil) +} + +// WriteFloat64 writes a float64 to the writer +func (mw *Writer) WriteFloat64(f float64) error { + return mw.prefix64(mfloat64, math.Float64bits(f)) +} + +// WriteFloat32 writes a float32 to the writer +func (mw *Writer) WriteFloat32(f float32) error { + return mw.prefix32(mfloat32, math.Float32bits(f)) +} + +// WriteDuration writes a time.Duration to the writer +func (mw *Writer) WriteDuration(d time.Duration) error { + return mw.WriteInt64(int64(d)) +} + +// WriteInt64 writes an int64 to the writer +func (mw *Writer) WriteInt64(i int64) error { + if i >= 0 { + switch { + case i <= math.MaxInt8: + return mw.push(wfixint(uint8(i))) + case i <= math.MaxInt16: + return mw.prefix16(mint16, uint16(i)) + case i <= math.MaxInt32: + return mw.prefix32(mint32, uint32(i)) + default: + return mw.prefix64(mint64, uint64(i)) + } + } + switch { + case i >= -32: + return mw.push(wnfixint(int8(i))) + case i >= math.MinInt8: + return mw.prefix8(mint8, uint8(i)) + case i >= math.MinInt16: + return mw.prefix16(mint16, uint16(i)) + case i >= math.MinInt32: + return mw.prefix32(mint32, uint32(i)) + default: + return mw.prefix64(mint64, uint64(i)) + } +} + +// WriteInt8 writes an int8 to the writer +func (mw *Writer) WriteInt8(i int8) error { return mw.WriteInt64(int64(i)) } + +// WriteInt16 writes an int16 to the writer +func (mw *Writer) WriteInt16(i int16) error { return mw.WriteInt64(int64(i)) } + +// WriteInt32 writes an int32 to the writer +func (mw *Writer) WriteInt32(i int32) error { return mw.WriteInt64(int64(i)) } + +// WriteInt writes an int to the writer +func (mw *Writer) WriteInt(i int) error { return mw.WriteInt64(int64(i)) } + +// WriteUint64 writes a uint64 to the writer +func (mw *Writer) WriteUint64(u uint64) error { + switch { + case u <= (1<<7)-1: + return mw.push(wfixint(uint8(u))) + case u <= math.MaxUint8: + return mw.prefix8(muint8, uint8(u)) + case u <= math.MaxUint16: + return mw.prefix16(muint16, uint16(u)) + case u <= math.MaxUint32: + return mw.prefix32(muint32, uint32(u)) + default: + return mw.prefix64(muint64, u) + } +} + +// WriteByte is analogous to WriteUint8 +func (mw *Writer) WriteByte(u byte) error { return mw.WriteUint8(uint8(u)) } + +// WriteUint8 writes a uint8 to the writer +func (mw *Writer) WriteUint8(u uint8) error { return mw.WriteUint64(uint64(u)) } + +// WriteUint16 writes a uint16 to the writer +func (mw *Writer) WriteUint16(u uint16) error { return mw.WriteUint64(uint64(u)) } + +// WriteUint32 writes a uint32 to the writer +func (mw *Writer) WriteUint32(u uint32) error { return mw.WriteUint64(uint64(u)) } + +// WriteUint writes a uint to the writer +func (mw *Writer) WriteUint(u uint) error { return mw.WriteUint64(uint64(u)) } + +// WriteBytes writes binary as 'bin' to the writer +func (mw *Writer) WriteBytes(b []byte) error { + sz := uint32(len(b)) + var err error + switch { + case sz <= math.MaxUint8: + err = mw.prefix8(mbin8, uint8(sz)) + case sz <= math.MaxUint16: + err = mw.prefix16(mbin16, uint16(sz)) + default: + err = mw.prefix32(mbin32, sz) + } + if err != nil { + return err + } + _, err = mw.Write(b) + return err +} + +// WriteBytesHeader writes just the size header +// of a MessagePack 'bin' object. The user is responsible +// for then writing 'sz' more bytes into the stream. +func (mw *Writer) WriteBytesHeader(sz uint32) error { + switch { + case sz <= math.MaxUint8: + return mw.prefix8(mbin8, uint8(sz)) + case sz <= math.MaxUint16: + return mw.prefix16(mbin16, uint16(sz)) + default: + return mw.prefix32(mbin32, sz) + } +} + +// WriteBool writes a bool to the writer +func (mw *Writer) WriteBool(b bool) error { + if b { + return mw.push(mtrue) + } + return mw.push(mfalse) +} + +// WriteString writes a messagepack string to the writer. +// (This is NOT an implementation of io.StringWriter) +func (mw *Writer) WriteString(s string) error { + sz := uint32(len(s)) + var err error + switch { + case sz <= 31: + err = mw.push(wfixstr(uint8(sz))) + case sz <= math.MaxUint8: + err = mw.prefix8(mstr8, uint8(sz)) + case sz <= math.MaxUint16: + err = mw.prefix16(mstr16, uint16(sz)) + default: + err = mw.prefix32(mstr32, sz) + } + if err != nil { + return err + } + return mw.writeString(s) +} + +// WriteStringHeader writes just the string size +// header of a MessagePack 'str' object. The user +// is responsible for writing 'sz' more valid UTF-8 +// bytes to the stream. +func (mw *Writer) WriteStringHeader(sz uint32) error { + switch { + case sz <= 31: + return mw.push(wfixstr(uint8(sz))) + case sz <= math.MaxUint8: + return mw.prefix8(mstr8, uint8(sz)) + case sz <= math.MaxUint16: + return mw.prefix16(mstr16, uint16(sz)) + default: + return mw.prefix32(mstr32, sz) + } +} + +// WriteStringFromBytes writes a 'str' object +// from a []byte. +func (mw *Writer) WriteStringFromBytes(str []byte) error { + sz := uint32(len(str)) + var err error + switch { + case sz <= 31: + err = mw.push(wfixstr(uint8(sz))) + case sz <= math.MaxUint8: + err = mw.prefix8(mstr8, uint8(sz)) + case sz <= math.MaxUint16: + err = mw.prefix16(mstr16, uint16(sz)) + default: + err = mw.prefix32(mstr32, sz) + } + if err != nil { + return err + } + _, err = mw.Write(str) + return err +} + +// WriteComplex64 writes a complex64 to the writer +func (mw *Writer) WriteComplex64(f complex64) error { + o, err := mw.require(10) + if err != nil { + return err + } + mw.buf[o] = mfixext8 + mw.buf[o+1] = Complex64Extension + big.PutUint32(mw.buf[o+2:], math.Float32bits(real(f))) + big.PutUint32(mw.buf[o+6:], math.Float32bits(imag(f))) + return nil +} + +// WriteComplex128 writes a complex128 to the writer +func (mw *Writer) WriteComplex128(f complex128) error { + o, err := mw.require(18) + if err != nil { + return err + } + mw.buf[o] = mfixext16 + mw.buf[o+1] = Complex128Extension + big.PutUint64(mw.buf[o+2:], math.Float64bits(real(f))) + big.PutUint64(mw.buf[o+10:], math.Float64bits(imag(f))) + return nil +} + +// WriteMapStrStr writes a map[string]string to the writer +func (mw *Writer) WriteMapStrStr(mp map[string]string) (err error) { + err = mw.WriteMapHeader(uint32(len(mp))) + if err != nil { + return + } + for key, val := range mp { + err = mw.WriteString(key) + if err != nil { + return + } + err = mw.WriteString(val) + if err != nil { + return + } + } + return nil +} + +// WriteMapStrIntf writes a map[string]interface to the writer +func (mw *Writer) WriteMapStrIntf(mp map[string]interface{}) (err error) { + err = mw.WriteMapHeader(uint32(len(mp))) + if err != nil { + return + } + for key, val := range mp { + err = mw.WriteString(key) + if err != nil { + return + } + err = mw.WriteIntf(val) + if err != nil { + return + } + } + return +} + +// WriteTime writes a time.Time object to the wire. +// +// Time is encoded as Unix time, which means that +// location (time zone) data is removed from the object. +// The encoded object itself is 12 bytes: 8 bytes for +// a big-endian 64-bit integer denoting seconds +// elapsed since "zero" Unix time, followed by 4 bytes +// for a big-endian 32-bit signed integer denoting +// the nanosecond offset of the time. This encoding +// is intended to ease portability across languages. +// (Note that this is *not* the standard time.Time +// binary encoding, because its implementation relies +// heavily on the internal representation used by the +// time package.) +func (mw *Writer) WriteTime(t time.Time) error { + t = t.UTC() + o, err := mw.require(15) + if err != nil { + return err + } + mw.buf[o] = mext8 + mw.buf[o+1] = 12 + mw.buf[o+2] = TimeExtension + putUnix(mw.buf[o+3:], t.Unix(), int32(t.Nanosecond())) + return nil +} + +// WriteIntf writes the concrete type of 'v'. +// WriteIntf will error if 'v' is not one of the following: +// - A bool, float, string, []byte, int, uint, or complex +// - A map of supported types (with string keys) +// - An array or slice of supported types +// - A pointer to a supported type +// - A type that satisfies the msgp.Encodable interface +// - A type that satisfies the msgp.Extension interface +func (mw *Writer) WriteIntf(v interface{}) error { + if v == nil { + return mw.WriteNil() + } + switch v := v.(type) { + + // preferred interfaces + + case Encodable: + return v.EncodeMsg(mw) + case Extension: + return mw.WriteExtension(v) + + // concrete types + + case bool: + return mw.WriteBool(v) + case float32: + return mw.WriteFloat32(v) + case float64: + return mw.WriteFloat64(v) + case complex64: + return mw.WriteComplex64(v) + case complex128: + return mw.WriteComplex128(v) + case uint8: + return mw.WriteUint8(v) + case uint16: + return mw.WriteUint16(v) + case uint32: + return mw.WriteUint32(v) + case uint64: + return mw.WriteUint64(v) + case uint: + return mw.WriteUint(v) + case int8: + return mw.WriteInt8(v) + case int16: + return mw.WriteInt16(v) + case int32: + return mw.WriteInt32(v) + case int64: + return mw.WriteInt64(v) + case int: + return mw.WriteInt(v) + case string: + return mw.WriteString(v) + case []byte: + return mw.WriteBytes(v) + case map[string]string: + return mw.WriteMapStrStr(v) + case map[string]interface{}: + return mw.WriteMapStrIntf(v) + case time.Time: + return mw.WriteTime(v) + case time.Duration: + return mw.WriteDuration(v) + } + + val := reflect.ValueOf(v) + if !isSupported(val.Kind()) || !val.IsValid() { + return errors.New("msgp: type " + val.String() + " not supported") + } + + switch val.Kind() { + case reflect.Ptr: + if val.IsNil() { + return mw.WriteNil() + } + return mw.WriteIntf(val.Elem().Interface()) + case reflect.Slice: + return mw.writeSlice(val) + case reflect.Map: + return mw.writeMap(val) + } + return &ErrUnsupportedType{T: val.Type()} +} + +func (mw *Writer) writeMap(v reflect.Value) (err error) { + if v.Type().Key().Kind() != reflect.String { + return errors.New("msgp: map keys must be strings") + } + ks := v.MapKeys() + err = mw.WriteMapHeader(uint32(len(ks))) + if err != nil { + return + } + for _, key := range ks { + val := v.MapIndex(key) + err = mw.WriteString(key.String()) + if err != nil { + return + } + err = mw.WriteIntf(val.Interface()) + if err != nil { + return + } + } + return +} + +func (mw *Writer) writeSlice(v reflect.Value) (err error) { + // is []byte + if v.Type().ConvertibleTo(btsType) { + return mw.WriteBytes(v.Bytes()) + } + + sz := uint32(v.Len()) + err = mw.WriteArrayHeader(sz) + if err != nil { + return + } + for i := uint32(0); i < sz; i++ { + err = mw.WriteIntf(v.Index(int(i)).Interface()) + if err != nil { + return + } + } + return +} + +// is the reflect.Kind encodable? +func isSupported(k reflect.Kind) bool { + switch k { + case reflect.Func, reflect.Chan, reflect.Invalid, reflect.UnsafePointer: + return false + default: + return true + } +} + +// GuessSize guesses the size of the underlying +// value of 'i'. If the underlying value is not +// a simple builtin (or []byte), GuessSize defaults +// to 512. +func GuessSize(i interface{}) int { + if i == nil { + return NilSize + } + + switch i := i.(type) { + case Sizer: + return i.Msgsize() + case Extension: + return ExtensionPrefixSize + i.Len() + case float64: + return Float64Size + case float32: + return Float32Size + case uint8, uint16, uint32, uint64, uint: + return UintSize + case int8, int16, int32, int64, int: + return IntSize + case []byte: + return BytesPrefixSize + len(i) + case string: + return StringPrefixSize + len(i) + case complex64: + return Complex64Size + case complex128: + return Complex128Size + case bool: + return BoolSize + case map[string]interface{}: + s := MapHeaderSize + for key, val := range i { + s += StringPrefixSize + len(key) + GuessSize(val) + } + return s + case map[string]string: + s := MapHeaderSize + for key, val := range i { + s += 2*StringPrefixSize + len(key) + len(val) + } + return s + default: + return 512 + } +} diff --git a/vendor/github.com/tinylib/msgp/msgp/write_bytes.go b/vendor/github.com/tinylib/msgp/msgp/write_bytes.go new file mode 100644 index 0000000..676a6ef --- /dev/null +++ b/vendor/github.com/tinylib/msgp/msgp/write_bytes.go @@ -0,0 +1,436 @@ +package msgp + +import ( + "math" + "reflect" + "time" +) + +// ensure 'sz' extra bytes in 'b' btw len(b) and cap(b) +func ensure(b []byte, sz int) ([]byte, int) { + l := len(b) + c := cap(b) + if c-l < sz { + o := make([]byte, (2*c)+sz) // exponential growth + n := copy(o, b) + return o[:n+sz], n + } + return b[:l+sz], l +} + +// AppendMapHeader appends a map header with the +// given size to the slice +func AppendMapHeader(b []byte, sz uint32) []byte { + switch { + case sz <= 15: + return append(b, wfixmap(uint8(sz))) + + case sz <= math.MaxUint16: + o, n := ensure(b, 3) + prefixu16(o[n:], mmap16, uint16(sz)) + return o + + default: + o, n := ensure(b, 5) + prefixu32(o[n:], mmap32, sz) + return o + } +} + +// AppendArrayHeader appends an array header with +// the given size to the slice +func AppendArrayHeader(b []byte, sz uint32) []byte { + switch { + case sz <= 15: + return append(b, wfixarray(uint8(sz))) + + case sz <= math.MaxUint16: + o, n := ensure(b, 3) + prefixu16(o[n:], marray16, uint16(sz)) + return o + + default: + o, n := ensure(b, 5) + prefixu32(o[n:], marray32, sz) + return o + } +} + +// AppendNil appends a 'nil' byte to the slice +func AppendNil(b []byte) []byte { return append(b, mnil) } + +// AppendFloat64 appends a float64 to the slice +func AppendFloat64(b []byte, f float64) []byte { + o, n := ensure(b, Float64Size) + prefixu64(o[n:], mfloat64, math.Float64bits(f)) + return o +} + +// AppendFloat32 appends a float32 to the slice +func AppendFloat32(b []byte, f float32) []byte { + o, n := ensure(b, Float32Size) + prefixu32(o[n:], mfloat32, math.Float32bits(f)) + return o +} + +// AppendDuration appends a time.Duration to the slice +func AppendDuration(b []byte, d time.Duration) []byte { + return AppendInt64(b, int64(d)) +} + +// AppendInt64 appends an int64 to the slice +func AppendInt64(b []byte, i int64) []byte { + if i >= 0 { + switch { + case i <= math.MaxInt8: + return append(b, wfixint(uint8(i))) + case i <= math.MaxInt16: + o, n := ensure(b, 3) + putMint16(o[n:], int16(i)) + return o + case i <= math.MaxInt32: + o, n := ensure(b, 5) + putMint32(o[n:], int32(i)) + return o + default: + o, n := ensure(b, 9) + putMint64(o[n:], i) + return o + } + } + switch { + case i >= -32: + return append(b, wnfixint(int8(i))) + case i >= math.MinInt8: + o, n := ensure(b, 2) + putMint8(o[n:], int8(i)) + return o + case i >= math.MinInt16: + o, n := ensure(b, 3) + putMint16(o[n:], int16(i)) + return o + case i >= math.MinInt32: + o, n := ensure(b, 5) + putMint32(o[n:], int32(i)) + return o + default: + o, n := ensure(b, 9) + putMint64(o[n:], i) + return o + } +} + +// AppendInt appends an int to the slice +func AppendInt(b []byte, i int) []byte { return AppendInt64(b, int64(i)) } + +// AppendInt8 appends an int8 to the slice +func AppendInt8(b []byte, i int8) []byte { return AppendInt64(b, int64(i)) } + +// AppendInt16 appends an int16 to the slice +func AppendInt16(b []byte, i int16) []byte { return AppendInt64(b, int64(i)) } + +// AppendInt32 appends an int32 to the slice +func AppendInt32(b []byte, i int32) []byte { return AppendInt64(b, int64(i)) } + +// AppendUint64 appends a uint64 to the slice +func AppendUint64(b []byte, u uint64) []byte { + switch { + case u <= (1<<7)-1: + return append(b, wfixint(uint8(u))) + + case u <= math.MaxUint8: + o, n := ensure(b, 2) + putMuint8(o[n:], uint8(u)) + return o + + case u <= math.MaxUint16: + o, n := ensure(b, 3) + putMuint16(o[n:], uint16(u)) + return o + + case u <= math.MaxUint32: + o, n := ensure(b, 5) + putMuint32(o[n:], uint32(u)) + return o + + default: + o, n := ensure(b, 9) + putMuint64(o[n:], u) + return o + + } +} + +// AppendUint appends a uint to the slice +func AppendUint(b []byte, u uint) []byte { return AppendUint64(b, uint64(u)) } + +// AppendUint8 appends a uint8 to the slice +func AppendUint8(b []byte, u uint8) []byte { return AppendUint64(b, uint64(u)) } + +// AppendByte is analogous to AppendUint8 +func AppendByte(b []byte, u byte) []byte { return AppendUint8(b, uint8(u)) } + +// AppendUint16 appends a uint16 to the slice +func AppendUint16(b []byte, u uint16) []byte { return AppendUint64(b, uint64(u)) } + +// AppendUint32 appends a uint32 to the slice +func AppendUint32(b []byte, u uint32) []byte { return AppendUint64(b, uint64(u)) } + +// AppendBytes appends bytes to the slice as MessagePack 'bin' data +func AppendBytes(b []byte, bts []byte) []byte { + sz := len(bts) + var o []byte + var n int + switch { + case sz <= math.MaxUint8: + o, n = ensure(b, 2+sz) + prefixu8(o[n:], mbin8, uint8(sz)) + n += 2 + case sz <= math.MaxUint16: + o, n = ensure(b, 3+sz) + prefixu16(o[n:], mbin16, uint16(sz)) + n += 3 + default: + o, n = ensure(b, 5+sz) + prefixu32(o[n:], mbin32, uint32(sz)) + n += 5 + } + return o[:n+copy(o[n:], bts)] +} + +// AppendBytesHeader appends an 'bin' header with +// the given size to the slice. +func AppendBytesHeader(b []byte, sz uint32) []byte { + var o []byte + var n int + switch { + case sz <= math.MaxUint8: + o, n = ensure(b, 2) + prefixu8(o[n:], mbin8, uint8(sz)) + return o + case sz <= math.MaxUint16: + o, n = ensure(b, 3) + prefixu16(o[n:], mbin16, uint16(sz)) + return o + } + o, n = ensure(b, 5) + prefixu32(o[n:], mbin32, sz) + return o +} + +// AppendBool appends a bool to the slice +func AppendBool(b []byte, t bool) []byte { + if t { + return append(b, mtrue) + } + return append(b, mfalse) +} + +// AppendString appends a string as a MessagePack 'str' to the slice +func AppendString(b []byte, s string) []byte { + sz := len(s) + var n int + var o []byte + switch { + case sz <= 31: + o, n = ensure(b, 1+sz) + o[n] = wfixstr(uint8(sz)) + n++ + case sz <= math.MaxUint8: + o, n = ensure(b, 2+sz) + prefixu8(o[n:], mstr8, uint8(sz)) + n += 2 + case sz <= math.MaxUint16: + o, n = ensure(b, 3+sz) + prefixu16(o[n:], mstr16, uint16(sz)) + n += 3 + default: + o, n = ensure(b, 5+sz) + prefixu32(o[n:], mstr32, uint32(sz)) + n += 5 + } + return o[:n+copy(o[n:], s)] +} + +// AppendStringFromBytes appends a []byte +// as a MessagePack 'str' to the slice 'b.' +func AppendStringFromBytes(b []byte, str []byte) []byte { + sz := len(str) + var n int + var o []byte + switch { + case sz <= 31: + o, n = ensure(b, 1+sz) + o[n] = wfixstr(uint8(sz)) + n++ + case sz <= math.MaxUint8: + o, n = ensure(b, 2+sz) + prefixu8(o[n:], mstr8, uint8(sz)) + n += 2 + case sz <= math.MaxUint16: + o, n = ensure(b, 3+sz) + prefixu16(o[n:], mstr16, uint16(sz)) + n += 3 + default: + o, n = ensure(b, 5+sz) + prefixu32(o[n:], mstr32, uint32(sz)) + n += 5 + } + return o[:n+copy(o[n:], str)] +} + +// AppendComplex64 appends a complex64 to the slice as a MessagePack extension +func AppendComplex64(b []byte, c complex64) []byte { + o, n := ensure(b, Complex64Size) + o[n] = mfixext8 + o[n+1] = Complex64Extension + big.PutUint32(o[n+2:], math.Float32bits(real(c))) + big.PutUint32(o[n+6:], math.Float32bits(imag(c))) + return o +} + +// AppendComplex128 appends a complex128 to the slice as a MessagePack extension +func AppendComplex128(b []byte, c complex128) []byte { + o, n := ensure(b, Complex128Size) + o[n] = mfixext16 + o[n+1] = Complex128Extension + big.PutUint64(o[n+2:], math.Float64bits(real(c))) + big.PutUint64(o[n+10:], math.Float64bits(imag(c))) + return o +} + +// AppendTime appends a time.Time to the slice as a MessagePack extension +func AppendTime(b []byte, t time.Time) []byte { + o, n := ensure(b, TimeSize) + t = t.UTC() + o[n] = mext8 + o[n+1] = 12 + o[n+2] = TimeExtension + putUnix(o[n+3:], t.Unix(), int32(t.Nanosecond())) + return o +} + +// AppendMapStrStr appends a map[string]string to the slice +// as a MessagePack map with 'str'-type keys and values +func AppendMapStrStr(b []byte, m map[string]string) []byte { + sz := uint32(len(m)) + b = AppendMapHeader(b, sz) + for key, val := range m { + b = AppendString(b, key) + b = AppendString(b, val) + } + return b +} + +// AppendMapStrIntf appends a map[string]interface{} to the slice +// as a MessagePack map with 'str'-type keys. +func AppendMapStrIntf(b []byte, m map[string]interface{}) ([]byte, error) { + sz := uint32(len(m)) + b = AppendMapHeader(b, sz) + var err error + for key, val := range m { + b = AppendString(b, key) + b, err = AppendIntf(b, val) + if err != nil { + return b, err + } + } + return b, nil +} + +// AppendIntf appends the concrete type of 'i' to the +// provided []byte. 'i' must be one of the following: +// - 'nil' +// - A bool, float, string, []byte, int, uint, or complex +// - A map[string]interface{} or map[string]string +// - A []T, where T is another supported type +// - A *T, where T is another supported type +// - A type that satisfieds the msgp.Marshaler interface +// - A type that satisfies the msgp.Extension interface +func AppendIntf(b []byte, i interface{}) ([]byte, error) { + if i == nil { + return AppendNil(b), nil + } + + // all the concrete types + // for which we have methods + switch i := i.(type) { + case Marshaler: + return i.MarshalMsg(b) + case Extension: + return AppendExtension(b, i) + case bool: + return AppendBool(b, i), nil + case float32: + return AppendFloat32(b, i), nil + case float64: + return AppendFloat64(b, i), nil + case complex64: + return AppendComplex64(b, i), nil + case complex128: + return AppendComplex128(b, i), nil + case string: + return AppendString(b, i), nil + case []byte: + return AppendBytes(b, i), nil + case int8: + return AppendInt8(b, i), nil + case int16: + return AppendInt16(b, i), nil + case int32: + return AppendInt32(b, i), nil + case int64: + return AppendInt64(b, i), nil + case int: + return AppendInt64(b, int64(i)), nil + case uint: + return AppendUint64(b, uint64(i)), nil + case uint8: + return AppendUint8(b, i), nil + case uint16: + return AppendUint16(b, i), nil + case uint32: + return AppendUint32(b, i), nil + case uint64: + return AppendUint64(b, i), nil + case time.Time: + return AppendTime(b, i), nil + case map[string]interface{}: + return AppendMapStrIntf(b, i) + case map[string]string: + return AppendMapStrStr(b, i), nil + case []interface{}: + b = AppendArrayHeader(b, uint32(len(i))) + var err error + for _, k := range i { + b, err = AppendIntf(b, k) + if err != nil { + return b, err + } + } + return b, nil + } + + var err error + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Array, reflect.Slice: + l := v.Len() + b = AppendArrayHeader(b, uint32(l)) + for i := 0; i < l; i++ { + b, err = AppendIntf(b, v.Index(i).Interface()) + if err != nil { + return b, err + } + } + return b, nil + case reflect.Ptr: + if v.IsNil() { + return AppendNil(b), err + } + b, err = AppendIntf(b, v.Elem().Interface()) + return b, err + default: + return b, &ErrUnsupportedType{T: v.Type()} + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 03bae13..d3b9228 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -72,8 +72,10 @@ github.com/gofiber/adaptor/v2 # github.com/gofiber/fiber/v2 v2.52.5 ## explicit; go 1.20 github.com/gofiber/fiber/v2 +github.com/gofiber/fiber/v2/internal/memory github.com/gofiber/fiber/v2/internal/schema github.com/gofiber/fiber/v2/log +github.com/gofiber/fiber/v2/middleware/limiter github.com/gofiber/fiber/v2/utils # github.com/golang/snappy v0.0.4 ## explicit @@ -134,6 +136,9 @@ github.com/openzipkin/zipkin-go/propagation github.com/openzipkin/zipkin-go/propagation/b3 github.com/openzipkin/zipkin-go/reporter github.com/openzipkin/zipkin-go/reporter/http +# github.com/philhofer/fwd v1.1.2 +## explicit; go 1.15 +github.com/philhofer/fwd # github.com/pkg/errors v0.9.1 ## explicit github.com/pkg/errors @@ -165,6 +170,9 @@ github.com/rs/zerolog github.com/rs/zerolog/internal/cbor github.com/rs/zerolog/internal/json github.com/rs/zerolog/log +# github.com/tinylib/msgp v1.1.8 +## explicit; go 1.15 +github.com/tinylib/msgp/msgp # github.com/uber/jaeger-client-go v2.30.0+incompatible ## explicit github.com/uber/jaeger-client-go From a1ebc1d2c7d6a0011d4d11f5b3f245c182bdbdc8 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 02:49:03 +0300 Subject: [PATCH 11/21] init eng version --- internal/start/multiplatform.go | 28 +- pkg/admin_client/adminpanel.go | 88 +-- pkg/admin_client/assets/build_pages.go | 22 +- .../pages/auth/{index.html => eng_index.html} | 0 .../assets/pages/auth/ru_index.html | 254 +++++++ pkg/admin_client/assets/pages/auth/style.css | 264 ------- .../{index.html => eng_index.html} | 0 .../assets/pages/developer_info/ru_index.html | 305 +++++++++ .../assets/pages/docs/eng_index.html | 232 +++++++ .../{index.html => eng_index.html} | 0 .../pages/game_collection/ru_index.html | 325 +++++++++ .../{index.html => eng_index.html} | 0 .../assets/pages/game_config/ru_index.html | 563 +++++++++++++++ .../game_info/{index.html => eng_index.html} | 0 .../assets/pages/game_info/ru_index.html | 647 ++++++++++++++++++ .../assets/pages/main/eng_index.html | 353 ++++++++++ .../pages/main/{index.html => ru_index.html} | 44 +- pkg/admin_client/assets/pages/pages.go | 28 +- 18 files changed, 2804 insertions(+), 349 deletions(-) rename pkg/admin_client/assets/pages/auth/{index.html => eng_index.html} (100%) create mode 100644 pkg/admin_client/assets/pages/auth/ru_index.html delete mode 100644 pkg/admin_client/assets/pages/auth/style.css rename pkg/admin_client/assets/pages/developer_info/{index.html => eng_index.html} (100%) create mode 100644 pkg/admin_client/assets/pages/developer_info/ru_index.html create mode 100644 pkg/admin_client/assets/pages/docs/eng_index.html rename pkg/admin_client/assets/pages/game_collection/{index.html => eng_index.html} (100%) create mode 100644 pkg/admin_client/assets/pages/game_collection/ru_index.html rename pkg/admin_client/assets/pages/game_config/{index.html => eng_index.html} (100%) create mode 100644 pkg/admin_client/assets/pages/game_config/ru_index.html rename pkg/admin_client/assets/pages/game_info/{index.html => eng_index.html} (100%) create mode 100644 pkg/admin_client/assets/pages/game_info/ru_index.html create mode 100644 pkg/admin_client/assets/pages/main/eng_index.html rename pkg/admin_client/assets/pages/main/{index.html => ru_index.html} (85%) diff --git a/internal/start/multiplatform.go b/internal/start/multiplatform.go index 8b063bc..bcdafbc 100644 --- a/internal/start/multiplatform.go +++ b/internal/start/multiplatform.go @@ -18,6 +18,7 @@ import ( "github.com/rs/zerolog" "html/template" "runtime" + "strings" "time" ) @@ -111,7 +112,7 @@ func mastNil(err error) { func adminPanel(app *fiber.App) { app.Get("/", func(c *fiber.Ctx) error { - tmpl, err := template.New("docs").Parse(string(adminclient.MainPage("ru"))) + tmpl, err := template.New("docs").Parse(string(adminclient.MainPage(detectLanguage(c)))) if err != nil { return err } @@ -124,7 +125,7 @@ func adminPanel(app *fiber.App) { }) app.Get("/developer/doc", func(c *fiber.Ctx) error { - tmpl, err := template.New("docs").Parse(string(adminclient.DevDocs("ru"))) + tmpl, err := template.New("docs").Parse(string(adminclient.DevDocs(detectLanguage(c)))) if err != nil { return err } @@ -138,7 +139,7 @@ func adminPanel(app *fiber.App) { }) app.Get("/admin/auth", func(c *fiber.Ctx) error { - tmpl, err := template.New("docs").Parse(string(adminclient.Auth("ru"))) + tmpl, err := template.New("docs").Parse(string(adminclient.Auth(detectLanguage(c)))) if err != nil { return err } @@ -151,7 +152,7 @@ func adminPanel(app *fiber.App) { }) app.Get("/admin/games", func(c *fiber.Ctx) error { - tmpl, err := template.New("docs").Parse(string(adminclient.GameCollection("ru"))) + tmpl, err := template.New("docs").Parse(string(adminclient.GameCollection(detectLanguage(c)))) if err != nil { return err } @@ -164,7 +165,7 @@ func adminPanel(app *fiber.App) { }) app.Get("/admin/game_info", func(c *fiber.Ctx) error { - tmpl, err := template.New("docs").Parse(string(adminclient.GameInfo("ru"))) + tmpl, err := template.New("docs").Parse(string(adminclient.GameInfo(detectLanguage(c)))) if err != nil { return err } @@ -177,7 +178,7 @@ func adminPanel(app *fiber.App) { }) app.Get("/admin/game_info/config", func(c *fiber.Ctx) error { - tmpl, err := template.New("docs").Parse(string(adminclient.GameConfig("ru"))) + tmpl, err := template.New("docs").Parse(string(adminclient.GameConfig(detectLanguage(c)))) if err != nil { return err } @@ -190,7 +191,7 @@ func adminPanel(app *fiber.App) { }) app.Get("/admin/info", func(c *fiber.Ctx) error { - tmpl, err := template.New("docs").Parse(string(adminclient.DeveloperInfo("ru"))) + tmpl, err := template.New("docs").Parse(string(adminclient.DeveloperInfo(detectLanguage(c)))) if err != nil { return err } @@ -202,3 +203,16 @@ func adminPanel(app *fiber.App) { return nil }) } + +func detectLanguage(c *fiber.Ctx) string { + acceptLanguage := c.Get("Accept-Language") + + admin := c.Cookies("AdminLanguageLanguage") + if admin != "" { + return admin + } + if strings.Contains(acceptLanguage, detectLanguage(c)) { + return adminclient.Ru + } + return adminclient.Eng +} diff --git a/pkg/admin_client/adminpanel.go b/pkg/admin_client/adminpanel.go index f7af265..adb7503 100644 --- a/pkg/admin_client/adminpanel.go +++ b/pkg/admin_client/adminpanel.go @@ -3,82 +3,82 @@ package adminclient import "github.com/ascenmmo/multiplayer-game-servers/pkg/admin_client/assets/pages" const ( - ru = "ru" - eng = "eng" + Ru = "ru" + Eng = "eng" ) -func DevDocs(lengudage string) string { - switch lengudage { - case ru: - return pages.Docs - case eng: - return pages.Docs +func DevDocs(lEngudage string) string { + switch lEngudage { + case Ru: + return pages.RuDocs + case Eng: + return pages.EngDocs default: return "not found" } } -func Auth(lengudage string) string { - switch lengudage { - case ru: - return pages.Auth - case eng: - return pages.Auth +func Auth(lEngudage string) string { + switch lEngudage { + case Ru: + return pages.RuAuth + case Eng: + return pages.EngAuth default: return "not found" } } -func GameCollection(lengudage string) string { - switch lengudage { - case ru: - return pages.GameCollection - case eng: - return pages.GameCollection +func GameCollection(lEngudage string) string { + switch lEngudage { + case Ru: + return pages.RuGameCollection + case Eng: + return pages.EngGameCollection default: return "not found" } } -func MainPage(lengudage string) string { - switch lengudage { - case ru: - return pages.MainPage - case eng: - return pages.MainPage +func MainPage(lEngudage string) string { + switch lEngudage { + case Ru: + return pages.RuMainPage + case Eng: + return pages.EngMainPage default: return "not found" } } -func GameInfo(lengudage string) string { - switch lengudage { - case ru: - return pages.GameInfo - case eng: - return pages.GameInfo +func GameInfo(lEngudage string) string { + switch lEngudage { + case Ru: + return pages.RuGameInfo + case Eng: + return pages.EngGameInfo default: return "not found" } } -func GameConfig(lengudage string) string { - switch lengudage { - case ru: - return pages.GameConfig - case eng: - return pages.GameConfig +func GameConfig(lEngudage string) string { + switch lEngudage { + case Ru: + return pages.RuGameConfig + case Eng: + return pages.EngGameConfig default: return "not found" } } -func DeveloperInfo(lengudage string) string { - switch lengudage { - case ru: - return pages.DeveloperInfo - case eng: - return pages.DeveloperInfo +func DeveloperInfo(lEngudage string) string { + switch lEngudage { + case Ru: + return pages.RuDeveloperInfo + case Eng: + return pages.EngDeveloperInfo default: return "not found" } diff --git a/pkg/admin_client/assets/build_pages.go b/pkg/admin_client/assets/build_pages.go index 5623756..f3e435c 100644 --- a/pkg/admin_client/assets/build_pages.go +++ b/pkg/admin_client/assets/build_pages.go @@ -11,13 +11,21 @@ const dir = "pkg/admin_client/assets/pages/" var ( indexFiles = map[string]string{ - "Auth": dir + "auth/index.html", - "Docs": dir + "docs/ru_index.html", - "GameCollection": dir + "game_collection/index.html", - "MainPage": dir + "main/index.html", - "GameInfo": dir + "game_info/index.html", - "GameConfig": dir + "game_config/index.html", - "DeveloperInfo": dir + "developer_info/index.html", + "RuAuth": dir + "auth/ru_index.html", + "RuDocs": dir + "docs/ru_index.html", + "RuGameCollection": dir + "game_collection/ru_index.html", + "RuMainPage": dir + "main/ru_index.html", + "RuGameInfo": dir + "game_info/ru_index.html", + "RuGameConfig": dir + "game_config/ru_index.html", + "RuDeveloperInfo": dir + "developer_info/ru_index.html", + + "EngAuth": dir + "auth/eng_index.html", + "EngDocs": dir + "docs/eng_index.html", + "EngGameCollection": dir + "game_collection/eng_index.html", + "EngMainPage": dir + "main/eng_index.html", + "EngGameInfo": dir + "game_info/eng_index.html", + "EngGameConfig": dir + "game_config/eng_index.html", + "EngDeveloperInfo": dir + "developer_info/eng_index.html", } ) diff --git a/pkg/admin_client/assets/pages/auth/index.html b/pkg/admin_client/assets/pages/auth/eng_index.html similarity index 100% rename from pkg/admin_client/assets/pages/auth/index.html rename to pkg/admin_client/assets/pages/auth/eng_index.html diff --git a/pkg/admin_client/assets/pages/auth/ru_index.html b/pkg/admin_client/assets/pages/auth/ru_index.html new file mode 100644 index 0000000..1c80438 --- /dev/null +++ b/pkg/admin_client/assets/pages/auth/ru_index.html @@ -0,0 +1,254 @@ + + + + + + Авторизация | ASCENMMO + + + + +
+

ASCENMMO

+ +
+ +
+

Авторизация ASCENMMO

+ +
+

Вход

+
+ + + +
+
+ + + + +
+ +
+

© 2024 Ascenmmo. Все права защищены.

+
+ + + + diff --git a/pkg/admin_client/assets/pages/auth/style.css b/pkg/admin_client/assets/pages/auth/style.css deleted file mode 100644 index 39d951b..0000000 --- a/pkg/admin_client/assets/pages/auth/style.css +++ /dev/null @@ -1,264 +0,0 @@ -body { - font-family: 'Arial', sans-serif; - margin: 0; - padding: 0; - background-color: #1c1c1c; - color: #fff; - line-height: 1.6; -} - -header { - position: fixed; - top: 0; - left: 0; - width: 100%; - padding: 20px; - background: rgba(0, 0, 0, 0.8); - display: flex; - align-items: center; - justify-content: space-between; - transition: background 0.3s ease; - z-index: 1000; -} - -header h1 { - margin: 0; - font-size: 24px; - font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */ - color: #00ff7f; - margin-left: 5%; /* Сдвиг логотипа на 10% влево */ -} - -header nav { - display: flex; - gap: 20px; - margin-right: 5%; /* Сдвиг кнопок на 10% вправо */ -} - -header nav a { - color: #fff; - text-decoration: none; - padding: 10px 20px; - border: 2px solid #00ff7f; - border-radius: 5px; - transition: background 0.3s, color 0.3s; -} - -header nav a:hover { - background: #00ff7f; - color: #1c1c1c; -} - -.hero { - height: 100vh; /* Высота экрана */ - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - color: #fff; - animation: fadeIn 1s ease-in; - width: 100%; /* Ширина на всю страницу */ - position: relative; /* Для абсолютного позиционирования дочерних элементов */ -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.future-container { - position: absolute; /* Задать абсолютное позиционирование */ - top: 0; /* Сдвиг вверх */ - left: 0; /* Сдвиг влево */ - background: #000; /* Черный фон */ - width: 100%; /* Ширина на всю страницу */ - height: 100%; /* Высота на всю страницу */ - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - color: #fff; - text-align: center; -} - -.future-container h2 { - font-size: 48px; - margin-bottom: 20px; -} - -.future-container p { - font-size: 24px; - margin-bottom: 30px; -} - -.button { - background: #00ff7f; - color: #1c1c1c; - border: none; - padding: 15px 30px; - border-radius: 5px; - font-size: 18px; - cursor: pointer; - transition: background 0.3s, transform 0.3s, box-shadow 0.3s; - width: 100%; /* Ширина кнопки на весь экран */ - max-width: 300px; /* Максимальная ширина кнопки */ -} - -.button:hover { - background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */ - transform: scale(1.05); - box-shadow: 0 0 20px rgba(0, 255, 127, 0.7); -} - -section { - padding: 60px 20px; - text-align: center; - animation: fadeIn 1s ease-in; -} - -h3 { - margin-bottom: 20px; - font-size: 36px; - color: #00ff7f; -} - -p { - margin-bottom: 40px; - font-size: 18px; - max-width: 600px; - margin-left: auto; - margin-right: auto; -} - -footer { - padding: 20px; - text-align: center; - background: #000; - position: relative; -} - -footer p { - margin: 0; -} - -.service-container { - display: flex; - justify-content: center; - gap: 20px; /* Пространство между элементами */ - flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */ - padding: 40px 20px; /* Отступы сверху и снизу */ -} - -.service-item { - background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */ - border-radius: 10px; - padding: 20px; - max-width: 300px; /* Максимальная ширина для каждого элемента */ - transition: transform 0.3s, box-shadow 0.3s; -} - -.service-item:hover { - transform: scale(1.05); /* Увеличение при наведении */ - box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */ -} - -.service-item h4 { - color: #00ff7f; /* Цвет заголовка */ - margin-bottom: 10px; /* Отступ снизу */ -} - -.service-item p { - font-size: 16px; /* Размер текста */ - color: #e0e0e0; /* Цвет текста для лучшего контраста */ -} - -#admin { - padding: 60px 20px; - text-align: center; - background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */ - border-radius: 10px; /* Закругление углов */ - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */ - position: relative; /* Для позиционирования псевдоэлементов */ - overflow: hidden; /* Скрытие переполненных элементов */ - animation: fadeIn 1s ease-in; /* Появление */ -} - -#admin h3 { - margin-bottom: 20px; - font-size: 36px; - color: #00ff7f; /* Цвет заголовка */ - animation: slideIn 0.5s ease; /* Анимация заголовка */ -} - -#admin p { - margin-bottom: 40px; - font-size: 20px; /* Увеличенный размер текста */ - color: #e0e0e0; /* Цвет текста для лучшего контраста */ -} - -.download-section { - background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */ - padding: 60px 20px; - text-align: center; - color: #fff; - animation: fadeIn 1s ease-in; /* Анимация появления */ - position: relative; /* Для абсолютного позиционирования внутренних элементов */ - overflow: hidden; /* Скрыть элементы, выходящие за пределы секции */ -} - -.download-section::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); /* Полупрозрачный черный фон для улучшения читаемости текста */ - z-index: 1; /* Поместить под контентом */ -} - -.download-content { - position: relative; /* Для обеспечения правильного позиционирования текста и кнопки */ - z-index: 2; /* Чтобы текст был выше фона */ -} - -.download-content h3 { - font-size: 36px; - margin-bottom: 20px; - animation: slideIn 0.5s ease-in-out; /* Анимация заголовка */ -} - -.download-content p { - font-size: 18px; - margin-bottom: 10px; /* Уменьшено расстояние между параграфами */ - animation: fadeIn 1s ease-in-out; /* Анимация текста */ -} - -.info-text { - font-size: 16px; - font-weight: bold; - margin-bottom: 30px; /* Добавлено расстояние под информационным текстом */ - color: #ffd700; /* Цвет текста для выделения */ -} - -/* Стили для формы авторизации и регистрации */ -#auth-section { - display: flex; - flex-direction: column; - align-items: center; - padding: 80px 20px; /* Отступы для секции */ -} - -#auth-section h1 { - font-size: 36px; - margin-bottom: 20px; - color: #00ff7f; -} - -#auth-section h2 { - font-size: 28px; - margin-bottom: 10px; - color: #00ff7f; /* Цвет заголовков форм */ -} - diff --git a/pkg/admin_client/assets/pages/developer_info/index.html b/pkg/admin_client/assets/pages/developer_info/eng_index.html similarity index 100% rename from pkg/admin_client/assets/pages/developer_info/index.html rename to pkg/admin_client/assets/pages/developer_info/eng_index.html diff --git a/pkg/admin_client/assets/pages/developer_info/ru_index.html b/pkg/admin_client/assets/pages/developer_info/ru_index.html new file mode 100644 index 0000000..dff0c41 --- /dev/null +++ b/pkg/admin_client/assets/pages/developer_info/ru_index.html @@ -0,0 +1,305 @@ + + + + + + + Информация о разработчике + + + +
+

ASCENMMO

+ +
+ +
+
+

Информация о разработчике

+
+ + + + + +

Ошибка при обновлении информации.

+
+ + +
+
+ +
+

© 2024 Ascenmmo. Все права защищены.

+
+ + + + diff --git a/pkg/admin_client/assets/pages/docs/eng_index.html b/pkg/admin_client/assets/pages/docs/eng_index.html new file mode 100644 index 0000000..fecb022 --- /dev/null +++ b/pkg/admin_client/assets/pages/docs/eng_index.html @@ -0,0 +1,232 @@ + + + + + + + Документация API + + +
+

ASCENMMO

+ +
+ +

Документация API

+{{ range . }} +
+
{{ .CategoryTitle }}
+
+ {{ range .DocStruct }} +
+
{{ .Title }}
+
+

{{ .Info }}

+ {{ if .DockLists }} +
Списки структур документа:
+ {{ range .DockLists }} +

{{ .Title }}

+

{{ .Description }}

+
Путь запроса:
+
{{ .RequestPath }}
+
Метод:
+
{{ .Method }}
+
Заголовки запроса:
+
{{ .RequestHeader }}
+
Тело запроса:
+
{{ .RequestBody }}
+
Информация о теле запроса:
+

{{ .RequestBodyInfo }}

+
Ответ:
+
{{ .ResponseBody }}
+
Информация о ответе:
+

{{ .ResponseBodyInfo }}

+ {{ end }} + {{ else }} +

Нет структур документа.

+ {{ end }} +
+
+ {{ end }} + + {{ if .DocErrorList }} +
Список ошибок:
+ {{ range .DocErrorList }} +
+
{{ .Name }}
+
+

{{ .Description }}

+
{{ .Body }}
+
+
+ {{ end }} + {{ else }} +

Ошибки не найдены.

+ {{ end }} +
+
+{{ end }} + +
+

© 2024 Ascenmmo. Все права защищены.

+
+ + + diff --git a/pkg/admin_client/assets/pages/game_collection/index.html b/pkg/admin_client/assets/pages/game_collection/eng_index.html similarity index 100% rename from pkg/admin_client/assets/pages/game_collection/index.html rename to pkg/admin_client/assets/pages/game_collection/eng_index.html diff --git a/pkg/admin_client/assets/pages/game_collection/ru_index.html b/pkg/admin_client/assets/pages/game_collection/ru_index.html new file mode 100644 index 0000000..b2b4ba5 --- /dev/null +++ b/pkg/admin_client/assets/pages/game_collection/ru_index.html @@ -0,0 +1,325 @@ + + + + + + + Коллекция Игр + + + +
+

ASCENMMO

+ +
+ +
+

Коллекция Игр

+
+ +
+
+ + + +
+ +
+ +
+

© 2024 Ascenmmo. Все права защищены.

+
+ + + + diff --git a/pkg/admin_client/assets/pages/game_config/index.html b/pkg/admin_client/assets/pages/game_config/eng_index.html similarity index 100% rename from pkg/admin_client/assets/pages/game_config/index.html rename to pkg/admin_client/assets/pages/game_config/eng_index.html diff --git a/pkg/admin_client/assets/pages/game_config/ru_index.html b/pkg/admin_client/assets/pages/game_config/ru_index.html new file mode 100644 index 0000000..ef01d87 --- /dev/null +++ b/pkg/admin_client/assets/pages/game_config/ru_index.html @@ -0,0 +1,563 @@ + + + + + + + Конфигурация Игры + + + +
+

ASCENMMO

+ +
+ +
+
+

Конфигурации

+
+ +
+
+ +
+ +
+
+

Описание Конфигурации

+
+            
+            Выберите конфигурацию, чтобы увидеть её описание.
+        
+
+

Добавление Конфигурации

+ + +
+ + +
+ +
+ + +
+
+ + +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + + +
+ + +
+ + +
+
+
+ +
+

© 2024 Ascenmmo. Все права защищены.

+
+ + + + + + + + diff --git a/pkg/admin_client/assets/pages/game_info/index.html b/pkg/admin_client/assets/pages/game_info/eng_index.html similarity index 100% rename from pkg/admin_client/assets/pages/game_info/index.html rename to pkg/admin_client/assets/pages/game_info/eng_index.html diff --git a/pkg/admin_client/assets/pages/game_info/ru_index.html b/pkg/admin_client/assets/pages/game_info/ru_index.html new file mode 100644 index 0000000..5a0e32d --- /dev/null +++ b/pkg/admin_client/assets/pages/game_info/ru_index.html @@ -0,0 +1,647 @@ + + + + + + + + Информация об Игре + + + +
+

ASCENMMO

+ +
+ +
+
+

Информация об Игре

+
+ +
+ +
+

Серверы

+ +
+
+ +
+ +
+

© 2024 Ascenmmo. Все права защищены.

+
+ + + + diff --git a/pkg/admin_client/assets/pages/main/eng_index.html b/pkg/admin_client/assets/pages/main/eng_index.html new file mode 100644 index 0000000..a376159 --- /dev/null +++ b/pkg/admin_client/assets/pages/main/eng_index.html @@ -0,0 +1,353 @@ + + + + + + Ascenmmo + + + + +
+

ASCENMMO

+ +
+ + +
+
+

Откройте Будущее Кросс-Платформенных Игр

+

Бесплатные и Мощные Решения для Разработчиков Игр

+ +
+
+ + +
+

Что мы предлагаем?

+
+
+

🚀 UDP Service

+

Испытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит + для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без + задержек.

+
+
+

🌐 WebSocket Service

+

Оживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и + взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.

+
+
+

🔒 TCP Service

+

Наслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит + для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.

+
+
+
+ +
+

🎮 Ваша игра, ваш контроль

+

Присоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной + админкой вы можете легко добавить свою игру и получить уникальный ID.

+ Перейти в админку +
+ +
+
+

Готовы к запуску? 🚀

+

Скачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в + формировании следующего поколения игр!

+ +
+
+ +
+

© 2024 Ascenmmo. Все права защищены.

+
+ + + + diff --git a/pkg/admin_client/assets/pages/main/index.html b/pkg/admin_client/assets/pages/main/ru_index.html similarity index 85% rename from pkg/admin_client/assets/pages/main/index.html rename to pkg/admin_client/assets/pages/main/ru_index.html index 27a0c8c..f0aa88f 100644 --- a/pkg/admin_client/assets/pages/main/index.html +++ b/pkg/admin_client/assets/pages/main/ru_index.html @@ -198,14 +198,14 @@ #admin h3 { margin-bottom: 20px; font-size: 36px; - color: #00ff7f; /* Цвет заголовка */ - animation: slideIn 0.5s ease; /* Анимация заголовка */ + color: #00ff7f; + animation: slideIn 0.5s ease; } #admin p { margin-bottom: 40px; - font-size: 20px; /* Увеличенный размер текста */ - color: #e0e0e0; /* Цвет текста для лучшего контраста */ + font-size: 20px; + color: #e0e0e0; } .download-section { @@ -213,9 +213,9 @@ padding: 60px 20px; text-align: center; color: #fff; - animation: fadeIn 1s ease-in; /* Анимация появления */ - position: relative; /* Для абсолютного позиционирования внутренних элементов */ - overflow: hidden; /* Скрыть элементы, выходящие за пределы секции */ + animation: fadeIn 1s ease-in; + position: relative; + overflow: hidden; } .download-section::before { @@ -225,32 +225,32 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.5); /* Полупрозрачный черный фон для улучшения читаемости текста */ - z-index: 1; /* Поместить под контентом */ + background: rgba(0, 0, 0, 0.5); + z-index: 1; } .download-content { - position: relative; /* Для обеспечения правильного позиционирования текста и кнопки */ - z-index: 2; /* Чтобы текст был выше фона */ + position: relative; + z-index: 2; } .download-content h3 { font-size: 36px; margin-bottom: 20px; - animation: slideIn 0.5s ease-in-out; /* Анимация заголовка */ + animation: slideIn 0.5s ease-in-out; } .download-content p { font-size: 18px; - margin-bottom: 10px; /* Уменьшено расстояние между параграфами */ - animation: fadeIn 1s ease-in-out; /* Анимация текста */ + margin-bottom: 10px; + animation: fadeIn 1s ease-in-out; } .info-text { font-size: 16px; font-weight: bold; - margin-bottom: 30px; /* Добавлено расстояние под информационным текстом */ - color: #ffd700; /* Цвет текста для выделения */ + margin-bottom: 30px; + color: #ffd700; } @@ -317,7 +317,6 @@

Готовы к запуску? 🚀

diff --git a/pkg/admin_client/assets/pages/pages.go b/pkg/admin_client/assets/pages/pages.go index 2d32d29..103c1a5 100644 --- a/pkg/admin_client/assets/pages/pages.go +++ b/pkg/admin_client/assets/pages/pages.go @@ -1,15 +1,29 @@ package pages -const Auth = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eАвторизация | ASCENMMO\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c);\n color: #ffffff;\n display: flex;\n flex-direction: column;\n line-height: 1.6;\n min-height: 100vh;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #auth-section {\n padding-top: 100px; /* Отступ сверху для фиксированного заголовка */\n text-align: center;\n }\n\n h1 {\n font-size: 36px;\n color: #00ff7f;\n margin-bottom: 20px;\n }\n\n h2 {\n margin: 20px 0;\n color: #00ff7f;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n input {\n margin: 10px 0;\n padding: 10px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n width: 300px;\n color: #fff;\n background-color: #333;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .toggle-button {\n margin-top: 20px;\n background: transparent;\n border: 2px solid #00ff7f;\n color: #00ff7f;\n padding: 10px 20px;\n cursor: pointer;\n transition: background 0.3s, color 0.3s;\n }\n\n .toggle-button:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv id=\"auth-section\"\u003e\n \u003ch1\u003eАвторизация ASCENMMO\u003c/h1\u003e\n\n \u003cdiv id=\"signin-form\" style=\"display: block;\"\u003e\n \u003ch2\u003eВход\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"email\" id=\"login-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"login-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eВойти\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cdiv id=\"signup-form\" style=\"display: none;\"\u003e\n \u003ch2\u003eРегистрация\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"text\" id=\"signup-nickname\" placeholder=\"Никнейм\" required /\u003e\n \u003cinput type=\"email\" id=\"signup-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"signup-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eЗарегистрироваться\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cbutton class=\"toggle-button\" id=\"toggle-auth\" onclick=\"toggleForm()\"\u003eПереключиться на регистрацию\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n let isSignIn = true;\n document.cookie = `token=; path=/`;\n document.cookie = `refresh=; path=/`;\n\n // Определяем домен и порт динамически\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n // Функция для переключения форм (вход/регистрация)\n function toggleForm() {\n if (isSignIn) {\n document.getElementById('signin-form').style.display = 'none';\n document.getElementById('signup-form').style.display = 'block';\n document.getElementById('toggle-auth').textContent = 'Переключиться на вход';\n } else {\n document.getElementById('signin-form').style.display = 'block';\n document.getElementById('signup-form').style.display = 'none';\n document.getElementById('toggle-auth').textContent = 'Переключиться на регистрацию';\n }\n isSignIn = !isSignIn;\n }\n\n // Авторизация\n document.querySelector('#signin-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const email = document.getElementById('login-email').value;\n const password = document.getElementById('login-password').value;\n\n // Используем динамически сформированный URL для обращения к бэкенду\n const response = await fetch(`${backendUrl}/signIn`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка авторизации!\\n' +\n 'Не верный логин или пароль');\n }\n });\n\n // Регистрация\n document.querySelector('#signup-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const nickname = document.getElementById('signup-nickname').value;\n const email = document.getElementById('signup-email').value;\n const password = document.getElementById('signup-password').value;\n\n const response = await fetch(`${backendUrl}/signUp`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { nickname, email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка регистрации!\\nПопробуйте поменять mail');\n }\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngAuth = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eАвторизация | ASCENMMO\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c);\n color: #ffffff;\n display: flex;\n flex-direction: column;\n line-height: 1.6;\n min-height: 100vh;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #auth-section {\n padding-top: 100px; /* Отступ сверху для фиксированного заголовка */\n text-align: center;\n }\n\n h1 {\n font-size: 36px;\n color: #00ff7f;\n margin-bottom: 20px;\n }\n\n h2 {\n margin: 20px 0;\n color: #00ff7f;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n input {\n margin: 10px 0;\n padding: 10px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n width: 300px;\n color: #fff;\n background-color: #333;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .toggle-button {\n margin-top: 20px;\n background: transparent;\n border: 2px solid #00ff7f;\n color: #00ff7f;\n padding: 10px 20px;\n cursor: pointer;\n transition: background 0.3s, color 0.3s;\n }\n\n .toggle-button:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv id=\"auth-section\"\u003e\n \u003ch1\u003eАвторизация ASCENMMO\u003c/h1\u003e\n\n \u003cdiv id=\"signin-form\" style=\"display: block;\"\u003e\n \u003ch2\u003eВход\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"email\" id=\"login-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"login-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eВойти\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cdiv id=\"signup-form\" style=\"display: none;\"\u003e\n \u003ch2\u003eРегистрация\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"text\" id=\"signup-nickname\" placeholder=\"Никнейм\" required /\u003e\n \u003cinput type=\"email\" id=\"signup-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"signup-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eЗарегистрироваться\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cbutton class=\"toggle-button\" id=\"toggle-auth\" onclick=\"toggleForm()\"\u003eПереключиться на регистрацию\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n let isSignIn = true;\n document.cookie = `token=; path=/`;\n document.cookie = `refresh=; path=/`;\n\n // Определяем домен и порт динамически\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n // Функция для переключения форм (вход/регистрация)\n function toggleForm() {\n if (isSignIn) {\n document.getElementById('signin-form').style.display = 'none';\n document.getElementById('signup-form').style.display = 'block';\n document.getElementById('toggle-auth').textContent = 'Переключиться на вход';\n } else {\n document.getElementById('signin-form').style.display = 'block';\n document.getElementById('signup-form').style.display = 'none';\n document.getElementById('toggle-auth').textContent = 'Переключиться на регистрацию';\n }\n isSignIn = !isSignIn;\n }\n\n // Авторизация\n document.querySelector('#signin-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const email = document.getElementById('login-email').value;\n const password = document.getElementById('login-password').value;\n\n // Используем динамически сформированный URL для обращения к бэкенду\n const response = await fetch(`${backendUrl}/signIn`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка авторизации!\\n' +\n 'Не верный логин или пароль');\n }\n });\n\n // Регистрация\n document.querySelector('#signup-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const nickname = document.getElementById('signup-nickname').value;\n const email = document.getElementById('signup-email').value;\n const password = document.getElementById('signup-password').value;\n\n const response = await fetch(`${backendUrl}/signUp`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { nickname, email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка регистрации!\\nПопробуйте поменять mail');\n }\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const DeveloperInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eИнформация о разработчике\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n display: flex;\n flex-direction: column; /* Размещаем элементы в колонку */\n justify-content: center; /* Центрируем по вертикали */\n align-items: center; /* Центрируем по горизонтали */\n min-height: 100vh; /* Занимаем минимальную высоту окна */\n padding-bottom: 50px; /* Отступ для футера */\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 30px;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .box {\n background-color: #333; /* Цвет фона бокса */\n border-radius: 10px; /* Закругление углов */\n padding: 20px; /* Отступы внутри бокса */\n width: 600px; /* Ширина бокса */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для бокса */\n text-align: center;\n margin-top: 60px; /* Отступ сверху для свободного места под фиксированным заголовком */\n }\n\n input[type=\"text\"], input[type=\"password\"] {\n width: calc(100% - 20px); /* Учитываем отступы */\n padding: 10px;\n margin: 10px 0;\n border-radius: 5px;\n border: 2px solid #00ff7f;\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin: 10px 0; /* Отступы между кнопками */\n width: 100%; /* Заполняем весь доступный размер бокса */\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .error-message {\n color: red;\n display: none; /* Скрыто по умолчанию */\n }\n\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"box\"\u003e \u003c!-- Добавляем контейнер с классом box --\u003e\n \u003csection\u003e\n \u003ch3\u003eИнформация о разработчике\u003c/h3\u003e\n \u003cdiv id=\"developer-info\"\u003e\n \u003cinput type=\"text\" id=\"email\" placeholder=\"Email\"\u003e\n \u003cinput type=\"text\" id=\"nickname\" placeholder=\"Никнейм\"\u003e\n \u003cinput type=\"password\" id=\"old-password\" placeholder=\"Старый пароль\"\u003e\n \u003cinput type=\"password\" id=\"new-password\" placeholder=\"Новый пароль\"\u003e\n \u003cbutton id=\"update-developer\"\u003eИзменить информацию\u003c/button\u003e\n \u003cp class=\"error-message\" id=\"error-message\"\u003eОшибка при обновлении информации.\u003c/p\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"logout\"\u003eВыйти из админки\u003c/button\u003e \u003c!-- Кнопка выхода --\u003e\n \u003c/section\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const emailInput = document.getElementById('email');\n const nicknameInput = document.getElementById('nickname');\n const oldPasswordInput = document.getElementById('old-password');\n const newPasswordInput = document.getElementById('new-password');\n const updateDeveloperButton = document.getElementById('update-developer');\n const logoutButton = document.getElementById('logout');\n const errorMessage = document.getElementById('error-message');\n\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n let isRetred = false;\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return;\n }\n\n isRetred = true;\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc();\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.developer) {\n emailInput.value = data.result.developer.email;\n nicknameInput.value = data.result.developer.nickname;\n } else {\n if (data.error) {\n handleApiError(data.error, fetchDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const updateDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/updateDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n developer: {\n email: emailInput.value,\n nickname: nicknameInput.value,\n password: oldPasswordInput.value,\n new_password: newPasswordInput.value,\n }\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result) {\n alert('Информация успешно обновлена');\n } else {\n if (data.error) {\n handleApiError(data.error, updateDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n fetchDeveloperInfo();\n };\n\n const getCookie = (name) =\u003e {\n const parts = document.cookie.split(';');\n for (let i = 0; i \u003c parts.length; i++) {\n const part = parts[i].trim();\n if (part.indexOf(name + '=') === 0) return part.split('=')[1];\n }\n return null;\n };\n\n updateDeveloperButton.addEventListener('click', updateDeveloperInfo);\n logoutButton.addEventListener('click', () =\u003e {\n document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем токен\n document.cookie = 'refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем refresh токен\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n });\n\n fetchDeveloperInfo(); // Запрашиваем информацию о разработчике\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngDeveloperInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eИнформация о разработчике\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n display: flex;\n flex-direction: column; /* Размещаем элементы в колонку */\n justify-content: center; /* Центрируем по вертикали */\n align-items: center; /* Центрируем по горизонтали */\n min-height: 100vh; /* Занимаем минимальную высоту окна */\n padding-bottom: 50px; /* Отступ для футера */\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 30px;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .box {\n background-color: #333; /* Цвет фона бокса */\n border-radius: 10px; /* Закругление углов */\n padding: 20px; /* Отступы внутри бокса */\n width: 600px; /* Ширина бокса */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для бокса */\n text-align: center;\n margin-top: 60px; /* Отступ сверху для свободного места под фиксированным заголовком */\n }\n\n input[type=\"text\"], input[type=\"password\"] {\n width: calc(100% - 20px); /* Учитываем отступы */\n padding: 10px;\n margin: 10px 0;\n border-radius: 5px;\n border: 2px solid #00ff7f;\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin: 10px 0; /* Отступы между кнопками */\n width: 100%; /* Заполняем весь доступный размер бокса */\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .error-message {\n color: red;\n display: none; /* Скрыто по умолчанию */\n }\n\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"box\"\u003e \u003c!-- Добавляем контейнер с классом box --\u003e\n \u003csection\u003e\n \u003ch3\u003eИнформация о разработчике\u003c/h3\u003e\n \u003cdiv id=\"developer-info\"\u003e\n \u003cinput type=\"text\" id=\"email\" placeholder=\"Email\"\u003e\n \u003cinput type=\"text\" id=\"nickname\" placeholder=\"Никнейм\"\u003e\n \u003cinput type=\"password\" id=\"old-password\" placeholder=\"Старый пароль\"\u003e\n \u003cinput type=\"password\" id=\"new-password\" placeholder=\"Новый пароль\"\u003e\n \u003cbutton id=\"update-developer\"\u003eИзменить информацию\u003c/button\u003e\n \u003cp class=\"error-message\" id=\"error-message\"\u003eОшибка при обновлении информации.\u003c/p\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"logout\"\u003eВыйти из админки\u003c/button\u003e \u003c!-- Кнопка выхода --\u003e\n \u003c/section\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const emailInput = document.getElementById('email');\n const nicknameInput = document.getElementById('nickname');\n const oldPasswordInput = document.getElementById('old-password');\n const newPasswordInput = document.getElementById('new-password');\n const updateDeveloperButton = document.getElementById('update-developer');\n const logoutButton = document.getElementById('logout');\n const errorMessage = document.getElementById('error-message');\n\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n let isRetred = false;\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return;\n }\n\n isRetred = true;\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc();\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.developer) {\n emailInput.value = data.result.developer.email;\n nicknameInput.value = data.result.developer.nickname;\n } else {\n if (data.error) {\n handleApiError(data.error, fetchDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const updateDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/updateDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n developer: {\n email: emailInput.value,\n nickname: nicknameInput.value,\n password: oldPasswordInput.value,\n new_password: newPasswordInput.value,\n }\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result) {\n alert('Информация успешно обновлена');\n } else {\n if (data.error) {\n handleApiError(data.error, updateDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n fetchDeveloperInfo();\n };\n\n const getCookie = (name) =\u003e {\n const parts = document.cookie.split(';');\n for (let i = 0; i \u003c parts.length; i++) {\n const part = parts[i].trim();\n if (part.indexOf(name + '=') === 0) return part.split('=')[1];\n }\n return null;\n };\n\n updateDeveloperButton.addEventListener('click', updateDeveloperInfo);\n logoutButton.addEventListener('click', () =\u003e {\n document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем токен\n document.cookie = 'refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем refresh токен\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n });\n\n fetchDeveloperInfo(); // Запрашиваем информацию о разработчике\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const Docs = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eДокументация API\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c); /* Градиент фона */\n color: #ffffff; /* Цвет текста */\n display: flex;\n flex-direction: column;\n min-height: 100vh; /* Минимальная высота на весь экран */\n margin-bottom: 100px;\n margin-top: 100px;\n }\n\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n h1 {\n color: #00ff7f; /* Яркий цвет заголовка */\n text-align: center;\n margin-bottom: 20px;\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Тень для заголовка */\n }\n\n .accordion {\n background: #3a3a3a; /* Цвет фона аккордеона */\n border-radius: 5px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); /* Тень для аккордеона */\n }\n\n .accordion-title {\n padding: 15px;\n cursor: pointer;\n border-bottom: 1px solid #444; /* Углубленный цвет */\n background: #4a4a4a; /* Цвет заголовка аккордеона */\n font-weight: bold;\n transition: background 0.3s, color 0.3s;\n }\n\n .accordion-title:hover {\n background: #5a5a5a; /* Цвет при наведении на заголовок */\n color: #00ff7f; /* Цвет текста при наведении */\n }\n\n .accordion-content {\n display: none;\n padding: 10px;\n background: #2b2b2b; /* Цвет фона содержимого аккордеона */\n }\n\n .active {\n background: #6a6a6a; /* Цвет активного заголовка */\n color: #00ff7f; /* Цвет текста активного заголовка */\n }\n\n pre {\n background: #1e1e1e; /* Цвет фона для блоков кода */\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 3px;\n overflow-x: auto;\n color: #ffffff; /* Цвет текста в блоке кода */\n }\n\n h3, h4, h5 {\n color: #ffffff; /* Цвет заголовков внутри аккордеона */\n }\n\n p {\n color: #cccccc; /* Цвет обычного текста внутри аккордеона */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\u003cbody\u003e\n\u003ch1\u003eДокументация API\u003c/h1\u003e\n{{ range . }} \u003c!-- Обработка каждой категории --\u003e\n\u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .CategoryTitle }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n {{ range .DocStruct }} \u003c!-- Обработка каждого документа в категории --\u003e\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Title }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Info }}\u003c/p\u003e\n {{ if .DockLists }}\n \u003ch5\u003eСписки структур документа:\u003c/h5\u003e\n {{ range .DockLists }}\n \u003ch3\u003e{{ .Title }}\u003c/h3\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003ch5\u003eПуть запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestPath }}\u003c/pre\u003e\n \u003ch5\u003eМетод:\u003c/h5\u003e\n \u003cpre\u003e{{ .Method }}\u003c/pre\u003e\n \u003ch5\u003eЗаголовки запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestHeader }}\u003c/pre\u003e\n \u003ch5\u003eТело запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о теле запроса:\u003c/h5\u003e\n \u003cp\u003e{{ .RequestBodyInfo }}\u003c/p\u003e\n \u003ch5\u003eОтвет:\u003c/h5\u003e\n \u003cpre\u003e{{ .ResponseBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о ответе:\u003c/h5\u003e\n \u003cp\u003e{{ .ResponseBodyInfo }}\u003c/p\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eНет структур документа.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n\n {{ if .DocErrorList }}\n \u003ch5\u003eСписок ошибок:\u003c/h5\u003e\n {{ range .DocErrorList }}\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Name }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003cpre\u003e{{ .Body }}\u003c/pre\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eОшибки не найдены.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n\u003c/div\u003e\n{{ end }}\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\u003cscript\u003e\n const titles = document.querySelectorAll('.accordion-title');\n titles.forEach(title =\u003e {\n title.addEventListener('click', function() {\n this.classList.toggle('active');\n const content = this.nextElementSibling;\n content.style.display = content.style.display === 'block' ? 'none' : 'block';\n });\n });\n\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n `;\n }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngDocs = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eДокументация API\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c); /* Градиент фона */\n color: #ffffff; /* Цвет текста */\n display: flex;\n flex-direction: column;\n min-height: 100vh; /* Минимальная высота на весь экран */\n margin-bottom: 100px;\n margin-top: 100px;\n }\n\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n h1 {\n color: #00ff7f; /* Яркий цвет заголовка */\n text-align: center;\n margin-bottom: 20px;\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Тень для заголовка */\n }\n\n .accordion {\n background: #3a3a3a; /* Цвет фона аккордеона */\n border-radius: 5px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); /* Тень для аккордеона */\n }\n\n .accordion-title {\n padding: 15px;\n cursor: pointer;\n border-bottom: 1px solid #444; /* Углубленный цвет */\n background: #4a4a4a; /* Цвет заголовка аккордеона */\n font-weight: bold;\n transition: background 0.3s, color 0.3s;\n }\n\n .accordion-title:hover {\n background: #5a5a5a; /* Цвет при наведении на заголовок */\n color: #00ff7f; /* Цвет текста при наведении */\n }\n\n .accordion-content {\n display: none;\n padding: 10px;\n background: #2b2b2b; /* Цвет фона содержимого аккордеона */\n }\n\n .active {\n background: #6a6a6a; /* Цвет активного заголовка */\n color: #00ff7f; /* Цвет текста активного заголовка */\n }\n\n pre {\n background: #1e1e1e; /* Цвет фона для блоков кода */\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 3px;\n overflow-x: auto;\n color: #ffffff; /* Цвет текста в блоке кода */\n }\n\n h3, h4, h5 {\n color: #ffffff; /* Цвет заголовков внутри аккордеона */\n }\n\n p {\n color: #cccccc; /* Цвет обычного текста внутри аккордеона */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\u003cbody\u003e\n\u003ch1\u003eДокументация API\u003c/h1\u003e\n{{ range . }} \u003c!-- Обработка каждой категории --\u003e\n\u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .CategoryTitle }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n {{ range .DocStruct }} \u003c!-- Обработка каждого документа в категории --\u003e\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Title }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Info }}\u003c/p\u003e\n {{ if .DockLists }}\n \u003ch5\u003eСписки структур документа:\u003c/h5\u003e\n {{ range .DockLists }}\n \u003ch3\u003e{{ .Title }}\u003c/h3\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003ch5\u003eПуть запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestPath }}\u003c/pre\u003e\n \u003ch5\u003eМетод:\u003c/h5\u003e\n \u003cpre\u003e{{ .Method }}\u003c/pre\u003e\n \u003ch5\u003eЗаголовки запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestHeader }}\u003c/pre\u003e\n \u003ch5\u003eТело запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о теле запроса:\u003c/h5\u003e\n \u003cp\u003e{{ .RequestBodyInfo }}\u003c/p\u003e\n \u003ch5\u003eОтвет:\u003c/h5\u003e\n \u003cpre\u003e{{ .ResponseBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о ответе:\u003c/h5\u003e\n \u003cp\u003e{{ .ResponseBodyInfo }}\u003c/p\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eНет структур документа.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n\n {{ if .DocErrorList }}\n \u003ch5\u003eСписок ошибок:\u003c/h5\u003e\n {{ range .DocErrorList }}\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Name }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003cpre\u003e{{ .Body }}\u003c/pre\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eОшибки не найдены.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n\u003c/div\u003e\n{{ end }}\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\u003cscript\u003e\n const titles = document.querySelectorAll('.accordion-title');\n titles.forEach(title =\u003e {\n title.addEventListener('click', function() {\n this.classList.toggle('active');\n const content = this.nextElementSibling;\n content.style.display = content.style.display === 'block' ? 'none' : 'block';\n });\n });\n\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n `;\n }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const GameCollection = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКоллекция Игр\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 5% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #games-collection {\n padding: 80px 20px; /* Отступ для секции */\n text-align: center;\n }\n\n #games-box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px; /* Пробел между играми */\n }\n\n .game-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n cursor: pointer; /* Курсор в виде указателя */\n }\n\n .game-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .button-container {\n margin-top: 20px; /* Отступ сверху для кнопок */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"games-collection\"\u003e\n \u003ch3\u003eКоллекция Игр\u003c/h3\u003e\n \u003cdiv id=\"games-box\"\u003e\n \u003c!-- Здесь будет отображаться список игр --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-game\"\u003eДобавить игру\u003c/button\u003e\n \u003cbutton id=\"refresh-list\"\u003eОбновить список\u003c/button\u003e\n \u003cbutton id=\"delete-game\"\u003eУдалить игру\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eИгры не найдены.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gamesBox = document.getElementById('games-box');\n const errorMessage = document.getElementById('error-message');\n const addGameButton = document.getElementById('add-game');\n const refreshListButton = document.getElementById('refresh-list');\n const deleteGameButton = document.getElementById('delete-game');\n const developerInfoButton = document.getElementById('developer-info');\n\n // Определяем базовый URL для бэкенда\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGames = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getMyGames`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.games.length \u003e 0) {\n errorMessage.style.display = 'none'; // Скрываем сообщение об ошибке\n renderGames(data.result.games);\n } else {\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = ''; // Очищаем контейнер с играми\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const deleteGameByID = async () =\u003e {\n const deletingID = prompt('Введите gameID\\n\\n' +\n 'Помните что после удаления игры все пользователи автоматически удаляться\\n' +\n 'Все информация об игре будет удаленна безвозвратно');\n\n if (!deletingID) {\n return\n }\n\n try {\n const response = await fetch(`${backendUrl}/deleteGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID: deletingID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = '';\n errorMessage.style.display = 'block';\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block';\n }\n fetchGames();\n };\n\n const renderGames = (games) =\u003e {\n gamesBox.innerHTML = ''; // Очищаем контейнер перед добавлением новых игр\n games.forEach(game =\u003e {\n const gameItem = document.createElement('div');\n gameItem.className = 'game-item';\n gameItem.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n \u003cp\u003eКоличество игроков: ${game.count_players}\u003c/p\u003e\n \u003cp\u003eКоличество игроков онлайн: ${game.count_online}\u003c/p\u003e\n `;\n gameItem.onclick = () =\u003e {\n window.location.href = `game_info?gameID=${game.gameID}`; // Редирект на страницу с игрой\n };\n gamesBox.appendChild(gameItem);\n });\n };\n\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n addGameButton.onclick = () =\u003e {\n window.location.href = 'game_info'; // Редирект на страницу добавления игры\n };\n\n refreshListButton.onclick = fetchGames;\n deleteGameButton.onclick = deleteGameByID;\n\n fetchGames();\n\n developerInfoButton.onclick = () =\u003e {\n window.location.href = 'developer-info';\n };\n\n // Первоначальный вызов для загрузки игр\n\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngGameCollection = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКоллекция Игр\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 5% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #games-collection {\n padding: 80px 20px; /* Отступ для секции */\n text-align: center;\n }\n\n #games-box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px; /* Пробел между играми */\n }\n\n .game-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n cursor: pointer; /* Курсор в виде указателя */\n }\n\n .game-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .button-container {\n margin-top: 20px; /* Отступ сверху для кнопок */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"games-collection\"\u003e\n \u003ch3\u003eКоллекция Игр\u003c/h3\u003e\n \u003cdiv id=\"games-box\"\u003e\n \u003c!-- Здесь будет отображаться список игр --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-game\"\u003eДобавить игру\u003c/button\u003e\n \u003cbutton id=\"refresh-list\"\u003eОбновить список\u003c/button\u003e\n \u003cbutton id=\"delete-game\"\u003eУдалить игру\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eИгры не найдены.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gamesBox = document.getElementById('games-box');\n const errorMessage = document.getElementById('error-message');\n const addGameButton = document.getElementById('add-game');\n const refreshListButton = document.getElementById('refresh-list');\n const deleteGameButton = document.getElementById('delete-game');\n const developerInfoButton = document.getElementById('developer-info');\n\n // Определяем базовый URL для бэкенда\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGames = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getMyGames`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.games.length \u003e 0) {\n errorMessage.style.display = 'none'; // Скрываем сообщение об ошибке\n renderGames(data.result.games);\n } else {\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = ''; // Очищаем контейнер с играми\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const deleteGameByID = async () =\u003e {\n const deletingID = prompt('Введите gameID\\n\\n' +\n 'Помните что после удаления игры все пользователи автоматически удаляться\\n' +\n 'Все информация об игре будет удаленна безвозвратно');\n\n if (!deletingID) {\n return\n }\n\n try {\n const response = await fetch(`${backendUrl}/deleteGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID: deletingID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = '';\n errorMessage.style.display = 'block';\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block';\n }\n fetchGames();\n };\n\n const renderGames = (games) =\u003e {\n gamesBox.innerHTML = ''; // Очищаем контейнер перед добавлением новых игр\n games.forEach(game =\u003e {\n const gameItem = document.createElement('div');\n gameItem.className = 'game-item';\n gameItem.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n \u003cp\u003eКоличество игроков: ${game.count_players}\u003c/p\u003e\n \u003cp\u003eКоличество игроков онлайн: ${game.count_online}\u003c/p\u003e\n `;\n gameItem.onclick = () =\u003e {\n window.location.href = `game_info?gameID=${game.gameID}`; // Редирект на страницу с игрой\n };\n gamesBox.appendChild(gameItem);\n });\n };\n\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n addGameButton.onclick = () =\u003e {\n window.location.href = 'game_info'; // Редирект на страницу добавления игры\n };\n\n refreshListButton.onclick = fetchGames;\n deleteGameButton.onclick = deleteGameByID;\n\n fetchGames();\n\n developerInfoButton.onclick = () =\u003e {\n window.location.href = 'developer-info';\n };\n\n // Первоначальный вызов для загрузки игр\n\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const GameConfig = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКонфигурация Игры\u003c/title\u003e\n \u003cstyle\u003e\n /* Основные стили */\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #config-section {\n padding: 80px 20px;\n display: flex;\n }\n\n #config-list {\n width: 30%;\n padding-right: 20px;\n }\n\n\n #config-description {\n width: 70%;\n border-left: 1px solid #00ff7f;\n padding-left: 20px;\n }\n\n .config-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n padding: 15px;\n margin-bottom: 10px;\n cursor: pointer;\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .config-item:hover {\n transform: scale(1.05);\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5);\n }\n\n .button-container {\n margin-top: 20px;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n pre {\n background: rgba(255, 255, 255, 0.1);\n padding: 10px;\n border-radius: 5px;\n overflow-x: auto;\n }\n\n .config-form {\n display: none;\n margin-top: 20px;\n background: rgba(255, 255, 255, 0.1);\n padding: 15px;\n border-radius: 10px;\n }\n\n .form-group {\n margin-bottom: 15px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 5px;\n }\n\n .form-group input,\n .form-group select {\n width: calc(20%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n font-size: 12px;\n }\n\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"config-section\"\u003e\n \u003cdiv id=\"config-list\"\u003e\n \u003ch3\u003eКонфигурации\u003c/h3\u003e\n \u003cdiv id=\"configs-box\"\u003e\n \u003c!-- Список конфигураций --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-config\"\u003eДобавить конфигурацию\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eКонфигурации не найдены.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv id=\"config-description\"\u003e\n \u003ch3\u003eОписание Конфигурации\u003c/h3\u003e\n \u003cpre id=\"config-json\"\u003e\n \u003cbutton id=\"refresh-result\" style=\"margin: 10px; float: right;\"\u003eОбновить результат\u003c/button\u003e\n Выберите конфигурацию, чтобы увидеть её описание.\n \u003c/pre\u003e\n \u003cdiv class=\"config-form\" id=\"config-form\"\u003e\n \u003ch3\u003eДобавление Конфигурации\u003c/h3\u003e\n\n \u003c!-- Выпадающий список действий --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"run_func\"\u003eВыберите действие:\u003c/label\u003e\n \u003cselect id=\"run_func\"\u003e\n \u003coption value=\"IncrementResult\"\u003eДобовляем к полю +1 {IncrementResult}\u003c/option\u003e\n \u003coption value=\"DecrementResult\"\u003eУдаляем из поля -1 {DecrementResult}\u003c/option\u003e\n \u003coption value=\"AdditionDataResultToOld\"\u003eСуммируем результаты oldData + newData {AdditionDataResultToOld}\u003c/option\u003e\n \u003coption value=\"SubtractDataResultToOld\"\u003eВычитаем результаты oldData - newData {SubtractDataResultToOld}\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_name\"\u003eИмя результата:\u003c/label\u003e\n \u003cinput type=\"text\" id=\"result_name\" placeholder=\"Введите имя результата\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_type\"\u003eТип результата:\u003c/label\u003e\n \u003cselect id=\"result_type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003c!-- Параметры пары значений --\u003e\n \u003cdiv id=\"pairs-container\"\u003e\n \u003cdiv class=\"pair-fields\"\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cbutton id=\"add-pair\"\u003eДобавить пару\u003c/button\u003e\n\n \u003c!-- Выбор типа сервера --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"use_on_server_type\"\u003eТип сервера:\u003c/label\u003e\n \u003cselect id=\"use_on_server_type\"\u003e\n \u003coption value=\"UDP\"\u003eUDP\u003c/option\u003e\n \u003coption value=\"TCP\"\u003eTCP\u003c/option\u003e\n \u003coption value=\"WS\"\u003eWebSocket\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"save-config\"\u003eСохранить\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const configsBox = document.getElementById('configs-box');\n const errorMessage = document.getElementById('error-message');\n const addConfigButton = document.getElementById('add-config');\n const configJson = document.getElementById('config-json');\n const configForm = document.getElementById('config-form');\n const saveConfigButton = document.getElementById('save-config');\n const addPairButton = document.getElementById('add-pair');\n const pairsContainer = document.getElementById('pairs-container');\n const refreshResultButton = document.getElementById('refresh-result'); // Кнопка обновления результата\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID'); // Получаем gameID из GET-параметра\n\n const backendUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameConfig`;\n const gameResultConfigUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameResultConfigPreview`;\n let existingConfigs = []; // Массив для хранения текущих конфигураций\n\n // Добавляем токен авторизации\n let token = getCookie(\"token\");\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n token = getCookie(\"token\")\n };\n\n // Получение текущих конфигураций\n function fetchGameConfig() {\n fetch(backendUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameConfig)\n }\n renderGameConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching configurations:', err);\n });\n }\n\n // Получение результата конфигурации игры\n function fetchGameResultConfigPreview() {\n fetch(gameResultConfigUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameResultConfigPreview)\n }\n renderGameResultConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching game result config:', err);\n });\n }\n\n // Отображение конфигураций\n function renderGameConfig(data) {\n if (!data.result || !data.result.configs || data.result.configs.sorting_config.length === 0) {\n errorMessage.style.display = 'block';\n } else {\n errorMessage.style.display = 'none';\n existingConfigs = data.result.configs.sorting_config; // Сохраняем текущие конфигурации\n configsBox.innerHTML = ''; // Очищаем перед рендерингом\n\n data.result.configs.sorting_config.forEach((config, index) =\u003e {\n const configItem = document.createElement('div');\n configItem.className = 'config-item';\n configItem.style.display = 'flex';\n configItem.style.alignItems = 'center'; // Выравнивание по центру\n\n // Кнопка для удаления конфигурации\n const deleteButton = document.createElement('button');\n deleteButton.textContent = 'X';\n deleteButton.style.marginRight = '10px'; // Отступ справа от кнопки\n deleteButton.style.backgroundColor = 'gray';\n deleteButton.style.color = '#fff';\n deleteButton.style.border = 'none';\n deleteButton.style.cursor = 'pointer';\n deleteButton.addEventListener('click', (e) =\u003e {\n e.stopPropagation(); // Чтобы не вызывалось событие клика на configItem\n deleteConfig(index);\n });\n\n configItem.appendChild(deleteButton); // Добавляем кнопку \"Удалить\" в элемент конфигурации\n configItem.appendChild(document.createTextNode(`${config.result_name}: ${config.name}`)); // Текст конфигурации\n\n configItem.addEventListener('click', () =\u003e {\n configJson.textContent = JSON.stringify(config, null, 2);\n configJson.appendChild(refreshResultButton);\n });\n configsBox.appendChild(configItem);\n });\n }\n }\n\n // Удаление конфигурации\n function deleteConfig(index) {\n existingConfigs.splice(index, 1); // Удаляем элемент по индексу\n update(); // Обновляем конфигурации на сервере\n }\n\n // Отображение результата конфигурации игры\n function renderGameResultConfig(data) {\n if (!data.result || !data.result.gameResult) {\n configJson.textContent = 'Нет данных для отображения.';\n configJson.appendChild(refreshResultButton);\n return;\n }\n\n const formattedJson = JSON.stringify(data.result.gameResult, null, 2);\n configJson.textContent = formattedJson;\n configJson.appendChild(refreshResultButton);\n }\n\n // Функция для обновления конфигураций на сервере\n function update() {\n const updatedConfigs = {\n gameID: gameID,\n sorting_config: existingConfigs\n };\n\n fetch(`${window.location.origin}/api/v1/devToolsGameConfigs/createOrUpdateConfig`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n configs: updatedConfigs\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, update)\n }\n })\n .catch(err =\u003e {\n\n console.error('Ошибка при отправке запроса:', err);\n })\n .finally(() =\u003e {\n fetchGameConfig(); // Обновляем конфигурации после обновления на сервере\n });\n }\n\n // Инициализация\n fetchGameConfig();\n fetchGameResultConfigPreview();\n\n // Обработчик для кнопки обновления результата\n refreshResultButton.addEventListener('click', fetchGameResultConfigPreview);\n\n addConfigButton.addEventListener('click', () =\u003e {\n configForm.style.display = 'block';\n });\n\n saveConfigButton.addEventListener('click', () =\u003e {\n const use_on_server_type = document.getElementById('use_on_server_type').value;\n const name = document.getElementById('run_func').value;\n const result_name = document.getElementById('result_name').value;\n const result_type = document.getElementById('result_type').value;\n\n // Собираем значения для параметров пар\n const pairs = Array.from(document.getElementsByClassName('pair-fields')).map(pairField =\u003e {\n const column_name = pairField.querySelector('.column-name').value;\n const value_type = pairField.querySelector('.value-type').value;\n return { column_name, value_type };\n });\n\n // Новая конфигурация\n const newConfig = {\n use_on_server_type: use_on_server_type,\n name: name,\n result_name: result_name,\n result_type: result_type,\n params: pairs\n };\n\n // Добавляем новую конфигурацию в массив existingConfigs\n existingConfigs.push(newConfig);\n update(); // Обновляем конфигурации на сервере после добавления\n });\n\n addPairButton.addEventListener('click', () =\u003e {\n const newPair = document.createElement('div');\n newPair.className = 'pair-fields';\n newPair.innerHTML = `\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n `;\n pairsContainer.appendChild(newPair);\n });\n\n function getCookie(name) {\n const matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n });\n\u003c/script\u003e\n\n\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngGameConfig = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКонфигурация Игры\u003c/title\u003e\n \u003cstyle\u003e\n /* Основные стили */\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #config-section {\n padding: 80px 20px;\n display: flex;\n }\n\n #config-list {\n width: 30%;\n padding-right: 20px;\n }\n\n\n #config-description {\n width: 70%;\n border-left: 1px solid #00ff7f;\n padding-left: 20px;\n }\n\n .config-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n padding: 15px;\n margin-bottom: 10px;\n cursor: pointer;\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .config-item:hover {\n transform: scale(1.05);\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5);\n }\n\n .button-container {\n margin-top: 20px;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n pre {\n background: rgba(255, 255, 255, 0.1);\n padding: 10px;\n border-radius: 5px;\n overflow-x: auto;\n }\n\n .config-form {\n display: none;\n margin-top: 20px;\n background: rgba(255, 255, 255, 0.1);\n padding: 15px;\n border-radius: 10px;\n }\n\n .form-group {\n margin-bottom: 15px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 5px;\n }\n\n .form-group input,\n .form-group select {\n width: calc(20%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n font-size: 12px;\n }\n\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"config-section\"\u003e\n \u003cdiv id=\"config-list\"\u003e\n \u003ch3\u003eКонфигурации\u003c/h3\u003e\n \u003cdiv id=\"configs-box\"\u003e\n \u003c!-- Список конфигураций --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-config\"\u003eДобавить конфигурацию\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eКонфигурации не найдены.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv id=\"config-description\"\u003e\n \u003ch3\u003eОписание Конфигурации\u003c/h3\u003e\n \u003cpre id=\"config-json\"\u003e\n \u003cbutton id=\"refresh-result\" style=\"margin: 10px; float: right;\"\u003eОбновить результат\u003c/button\u003e\n Выберите конфигурацию, чтобы увидеть её описание.\n \u003c/pre\u003e\n \u003cdiv class=\"config-form\" id=\"config-form\"\u003e\n \u003ch3\u003eДобавление Конфигурации\u003c/h3\u003e\n\n \u003c!-- Выпадающий список действий --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"run_func\"\u003eВыберите действие:\u003c/label\u003e\n \u003cselect id=\"run_func\"\u003e\n \u003coption value=\"IncrementResult\"\u003eДобовляем к полю +1 {IncrementResult}\u003c/option\u003e\n \u003coption value=\"DecrementResult\"\u003eУдаляем из поля -1 {DecrementResult}\u003c/option\u003e\n \u003coption value=\"AdditionDataResultToOld\"\u003eСуммируем результаты oldData + newData {AdditionDataResultToOld}\u003c/option\u003e\n \u003coption value=\"SubtractDataResultToOld\"\u003eВычитаем результаты oldData - newData {SubtractDataResultToOld}\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_name\"\u003eИмя результата:\u003c/label\u003e\n \u003cinput type=\"text\" id=\"result_name\" placeholder=\"Введите имя результата\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_type\"\u003eТип результата:\u003c/label\u003e\n \u003cselect id=\"result_type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003c!-- Параметры пары значений --\u003e\n \u003cdiv id=\"pairs-container\"\u003e\n \u003cdiv class=\"pair-fields\"\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cbutton id=\"add-pair\"\u003eДобавить пару\u003c/button\u003e\n\n \u003c!-- Выбор типа сервера --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"use_on_server_type\"\u003eТип сервера:\u003c/label\u003e\n \u003cselect id=\"use_on_server_type\"\u003e\n \u003coption value=\"UDP\"\u003eUDP\u003c/option\u003e\n \u003coption value=\"TCP\"\u003eTCP\u003c/option\u003e\n \u003coption value=\"WS\"\u003eWebSocket\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"save-config\"\u003eСохранить\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const configsBox = document.getElementById('configs-box');\n const errorMessage = document.getElementById('error-message');\n const addConfigButton = document.getElementById('add-config');\n const configJson = document.getElementById('config-json');\n const configForm = document.getElementById('config-form');\n const saveConfigButton = document.getElementById('save-config');\n const addPairButton = document.getElementById('add-pair');\n const pairsContainer = document.getElementById('pairs-container');\n const refreshResultButton = document.getElementById('refresh-result'); // Кнопка обновления результата\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID'); // Получаем gameID из GET-параметра\n\n const backendUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameConfig`;\n const gameResultConfigUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameResultConfigPreview`;\n let existingConfigs = []; // Массив для хранения текущих конфигураций\n\n // Добавляем токен авторизации\n let token = getCookie(\"token\");\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n token = getCookie(\"token\")\n };\n\n // Получение текущих конфигураций\n function fetchGameConfig() {\n fetch(backendUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameConfig)\n }\n renderGameConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching configurations:', err);\n });\n }\n\n // Получение результата конфигурации игры\n function fetchGameResultConfigPreview() {\n fetch(gameResultConfigUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameResultConfigPreview)\n }\n renderGameResultConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching game result config:', err);\n });\n }\n\n // Отображение конфигураций\n function renderGameConfig(data) {\n if (!data.result || !data.result.configs || data.result.configs.sorting_config.length === 0) {\n errorMessage.style.display = 'block';\n } else {\n errorMessage.style.display = 'none';\n existingConfigs = data.result.configs.sorting_config; // Сохраняем текущие конфигурации\n configsBox.innerHTML = ''; // Очищаем перед рендерингом\n\n data.result.configs.sorting_config.forEach((config, index) =\u003e {\n const configItem = document.createElement('div');\n configItem.className = 'config-item';\n configItem.style.display = 'flex';\n configItem.style.alignItems = 'center'; // Выравнивание по центру\n\n // Кнопка для удаления конфигурации\n const deleteButton = document.createElement('button');\n deleteButton.textContent = 'X';\n deleteButton.style.marginRight = '10px'; // Отступ справа от кнопки\n deleteButton.style.backgroundColor = 'gray';\n deleteButton.style.color = '#fff';\n deleteButton.style.border = 'none';\n deleteButton.style.cursor = 'pointer';\n deleteButton.addEventListener('click', (e) =\u003e {\n e.stopPropagation(); // Чтобы не вызывалось событие клика на configItem\n deleteConfig(index);\n });\n\n configItem.appendChild(deleteButton); // Добавляем кнопку \"Удалить\" в элемент конфигурации\n configItem.appendChild(document.createTextNode(`${config.result_name}: ${config.name}`)); // Текст конфигурации\n\n configItem.addEventListener('click', () =\u003e {\n configJson.textContent = JSON.stringify(config, null, 2);\n configJson.appendChild(refreshResultButton);\n });\n configsBox.appendChild(configItem);\n });\n }\n }\n\n // Удаление конфигурации\n function deleteConfig(index) {\n existingConfigs.splice(index, 1); // Удаляем элемент по индексу\n update(); // Обновляем конфигурации на сервере\n }\n\n // Отображение результата конфигурации игры\n function renderGameResultConfig(data) {\n if (!data.result || !data.result.gameResult) {\n configJson.textContent = 'Нет данных для отображения.';\n configJson.appendChild(refreshResultButton);\n return;\n }\n\n const formattedJson = JSON.stringify(data.result.gameResult, null, 2);\n configJson.textContent = formattedJson;\n configJson.appendChild(refreshResultButton);\n }\n\n // Функция для обновления конфигураций на сервере\n function update() {\n const updatedConfigs = {\n gameID: gameID,\n sorting_config: existingConfigs\n };\n\n fetch(`${window.location.origin}/api/v1/devToolsGameConfigs/createOrUpdateConfig`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n configs: updatedConfigs\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, update)\n }\n })\n .catch(err =\u003e {\n\n console.error('Ошибка при отправке запроса:', err);\n })\n .finally(() =\u003e {\n fetchGameConfig(); // Обновляем конфигурации после обновления на сервере\n });\n }\n\n // Инициализация\n fetchGameConfig();\n fetchGameResultConfigPreview();\n\n // Обработчик для кнопки обновления результата\n refreshResultButton.addEventListener('click', fetchGameResultConfigPreview);\n\n addConfigButton.addEventListener('click', () =\u003e {\n configForm.style.display = 'block';\n });\n\n saveConfigButton.addEventListener('click', () =\u003e {\n const use_on_server_type = document.getElementById('use_on_server_type').value;\n const name = document.getElementById('run_func').value;\n const result_name = document.getElementById('result_name').value;\n const result_type = document.getElementById('result_type').value;\n\n // Собираем значения для параметров пар\n const pairs = Array.from(document.getElementsByClassName('pair-fields')).map(pairField =\u003e {\n const column_name = pairField.querySelector('.column-name').value;\n const value_type = pairField.querySelector('.value-type').value;\n return { column_name, value_type };\n });\n\n // Новая конфигурация\n const newConfig = {\n use_on_server_type: use_on_server_type,\n name: name,\n result_name: result_name,\n result_type: result_type,\n params: pairs\n };\n\n // Добавляем новую конфигурацию в массив existingConfigs\n existingConfigs.push(newConfig);\n update(); // Обновляем конфигурации на сервере после добавления\n });\n\n addPairButton.addEventListener('click', () =\u003e {\n const newPair = document.createElement('div');\n newPair.className = 'pair-fields';\n newPair.innerHTML = `\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n `;\n pairsContainer.appendChild(newPair);\n });\n\n function getCookie(name) {\n const matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n });\n\u003c/script\u003e\n\n\n\u003c/body\u003e\n\u003c/html\u003e\n" -const GameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngGameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e--\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const MainPage = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f; /* Цвет заголовка */\n animation: slideIn 0.5s ease; /* Анимация заголовка */\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px; /* Увеличенный размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in; /* Анимация появления */\n position: relative; /* Для абсолютного позиционирования внутренних элементов */\n overflow: hidden; /* Скрыть элементы, выходящие за пределы секции */\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5); /* Полупрозрачный черный фон для улучшения читаемости текста */\n z-index: 1; /* Поместить под контентом */\n }\n\n .download-content {\n position: relative; /* Для обеспечения правильного позиционирования текста и кнопки */\n z-index: 2; /* Чтобы текст был выше фона */\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out; /* Анимация заголовка */\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px; /* Уменьшено расстояние между параграфами */\n animation: fadeIn 1s ease-in-out; /* Анимация текста */\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px; /* Добавлено расстояние под информационным текстом */\n color: #ffd700; /* Цвет текста для выделения */\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n // Функция для получения куки по имени\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n `;\n }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngMainPage = " \u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" + +const RuAuth = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eАвторизация | ASCENMMO\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c);\n color: #ffffff;\n display: flex;\n flex-direction: column;\n line-height: 1.6;\n min-height: 100vh;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #auth-section {\n padding-top: 100px; /* Отступ сверху для фиксированного заголовка */\n text-align: center;\n }\n\n h1 {\n font-size: 36px;\n color: #00ff7f;\n margin-bottom: 20px;\n }\n\n h2 {\n margin: 20px 0;\n color: #00ff7f;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n input {\n margin: 10px 0;\n padding: 10px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n width: 300px;\n color: #fff;\n background-color: #333;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .toggle-button {\n margin-top: 20px;\n background: transparent;\n border: 2px solid #00ff7f;\n color: #00ff7f;\n padding: 10px 20px;\n cursor: pointer;\n transition: background 0.3s, color 0.3s;\n }\n\n .toggle-button:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv id=\"auth-section\"\u003e\n \u003ch1\u003eАвторизация ASCENMMO\u003c/h1\u003e\n\n \u003cdiv id=\"signin-form\" style=\"display: block;\"\u003e\n \u003ch2\u003eВход\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"email\" id=\"login-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"login-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eВойти\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cdiv id=\"signup-form\" style=\"display: none;\"\u003e\n \u003ch2\u003eРегистрация\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"text\" id=\"signup-nickname\" placeholder=\"Никнейм\" required /\u003e\n \u003cinput type=\"email\" id=\"signup-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"signup-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eЗарегистрироваться\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cbutton class=\"toggle-button\" id=\"toggle-auth\" onclick=\"toggleForm()\"\u003eПереключиться на регистрацию\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n let isSignIn = true;\n document.cookie = `token=; path=/`;\n document.cookie = `refresh=; path=/`;\n\n // Определяем домен и порт динамически\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n // Функция для переключения форм (вход/регистрация)\n function toggleForm() {\n if (isSignIn) {\n document.getElementById('signin-form').style.display = 'none';\n document.getElementById('signup-form').style.display = 'block';\n document.getElementById('toggle-auth').textContent = 'Переключиться на вход';\n } else {\n document.getElementById('signin-form').style.display = 'block';\n document.getElementById('signup-form').style.display = 'none';\n document.getElementById('toggle-auth').textContent = 'Переключиться на регистрацию';\n }\n isSignIn = !isSignIn;\n }\n\n // Авторизация\n document.querySelector('#signin-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const email = document.getElementById('login-email').value;\n const password = document.getElementById('login-password').value;\n\n // Используем динамически сформированный URL для обращения к бэкенду\n const response = await fetch(`${backendUrl}/signIn`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка авторизации!\\n' +\n 'Не верный логин или пароль');\n }\n });\n\n // Регистрация\n document.querySelector('#signup-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const nickname = document.getElementById('signup-nickname').value;\n const email = document.getElementById('signup-email').value;\n const password = document.getElementById('signup-password').value;\n\n const response = await fetch(`${backendUrl}/signUp`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { nickname, email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка регистрации!\\nПопробуйте поменять mail');\n }\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" + +const RuDeveloperInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eИнформация о разработчике\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n display: flex;\n flex-direction: column; /* Размещаем элементы в колонку */\n justify-content: center; /* Центрируем по вертикали */\n align-items: center; /* Центрируем по горизонтали */\n min-height: 100vh; /* Занимаем минимальную высоту окна */\n padding-bottom: 50px; /* Отступ для футера */\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 30px;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .box {\n background-color: #333; /* Цвет фона бокса */\n border-radius: 10px; /* Закругление углов */\n padding: 20px; /* Отступы внутри бокса */\n width: 600px; /* Ширина бокса */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для бокса */\n text-align: center;\n margin-top: 60px; /* Отступ сверху для свободного места под фиксированным заголовком */\n }\n\n input[type=\"text\"], input[type=\"password\"] {\n width: calc(100% - 20px); /* Учитываем отступы */\n padding: 10px;\n margin: 10px 0;\n border-radius: 5px;\n border: 2px solid #00ff7f;\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin: 10px 0; /* Отступы между кнопками */\n width: 100%; /* Заполняем весь доступный размер бокса */\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .error-message {\n color: red;\n display: none; /* Скрыто по умолчанию */\n }\n\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"box\"\u003e \u003c!-- Добавляем контейнер с классом box --\u003e\n \u003csection\u003e\n \u003ch3\u003eИнформация о разработчике\u003c/h3\u003e\n \u003cdiv id=\"developer-info\"\u003e\n \u003cinput type=\"text\" id=\"email\" placeholder=\"Email\"\u003e\n \u003cinput type=\"text\" id=\"nickname\" placeholder=\"Никнейм\"\u003e\n \u003cinput type=\"password\" id=\"old-password\" placeholder=\"Старый пароль\"\u003e\n \u003cinput type=\"password\" id=\"new-password\" placeholder=\"Новый пароль\"\u003e\n \u003cbutton id=\"update-developer\"\u003eИзменить информацию\u003c/button\u003e\n \u003cp class=\"error-message\" id=\"error-message\"\u003eОшибка при обновлении информации.\u003c/p\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"logout\"\u003eВыйти из админки\u003c/button\u003e \u003c!-- Кнопка выхода --\u003e\n \u003c/section\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const emailInput = document.getElementById('email');\n const nicknameInput = document.getElementById('nickname');\n const oldPasswordInput = document.getElementById('old-password');\n const newPasswordInput = document.getElementById('new-password');\n const updateDeveloperButton = document.getElementById('update-developer');\n const logoutButton = document.getElementById('logout');\n const errorMessage = document.getElementById('error-message');\n\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n let isRetred = false;\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return;\n }\n\n isRetred = true;\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc();\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.developer) {\n emailInput.value = data.result.developer.email;\n nicknameInput.value = data.result.developer.nickname;\n } else {\n if (data.error) {\n handleApiError(data.error, fetchDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const updateDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/updateDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n developer: {\n email: emailInput.value,\n nickname: nicknameInput.value,\n password: oldPasswordInput.value,\n new_password: newPasswordInput.value,\n }\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result) {\n alert('Информация успешно обновлена');\n } else {\n if (data.error) {\n handleApiError(data.error, updateDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n fetchDeveloperInfo();\n };\n\n const getCookie = (name) =\u003e {\n const parts = document.cookie.split(';');\n for (let i = 0; i \u003c parts.length; i++) {\n const part = parts[i].trim();\n if (part.indexOf(name + '=') === 0) return part.split('=')[1];\n }\n return null;\n };\n\n updateDeveloperButton.addEventListener('click', updateDeveloperInfo);\n logoutButton.addEventListener('click', () =\u003e {\n document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем токен\n document.cookie = 'refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем refresh токен\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n });\n\n fetchDeveloperInfo(); // Запрашиваем информацию о разработчике\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" + +const RuDocs = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eДокументация API\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c); /* Градиент фона */\n color: #ffffff; /* Цвет текста */\n display: flex;\n flex-direction: column;\n min-height: 100vh; /* Минимальная высота на весь экран */\n margin-bottom: 100px;\n margin-top: 100px;\n }\n\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n h1 {\n color: #00ff7f; /* Яркий цвет заголовка */\n text-align: center;\n margin-bottom: 20px;\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Тень для заголовка */\n }\n\n .accordion {\n background: #3a3a3a; /* Цвет фона аккордеона */\n border-radius: 5px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); /* Тень для аккордеона */\n }\n\n .accordion-title {\n padding: 15px;\n cursor: pointer;\n border-bottom: 1px solid #444; /* Углубленный цвет */\n background: #4a4a4a; /* Цвет заголовка аккордеона */\n font-weight: bold;\n transition: background 0.3s, color 0.3s;\n }\n\n .accordion-title:hover {\n background: #5a5a5a; /* Цвет при наведении на заголовок */\n color: #00ff7f; /* Цвет текста при наведении */\n }\n\n .accordion-content {\n display: none;\n padding: 10px;\n background: #2b2b2b; /* Цвет фона содержимого аккордеона */\n }\n\n .active {\n background: #6a6a6a; /* Цвет активного заголовка */\n color: #00ff7f; /* Цвет текста активного заголовка */\n }\n\n pre {\n background: #1e1e1e; /* Цвет фона для блоков кода */\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 3px;\n overflow-x: auto;\n color: #ffffff; /* Цвет текста в блоке кода */\n }\n\n h3, h4, h5 {\n color: #ffffff; /* Цвет заголовков внутри аккордеона */\n }\n\n p {\n color: #cccccc; /* Цвет обычного текста внутри аккордеона */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\u003cbody\u003e\n\u003ch1\u003eДокументация API\u003c/h1\u003e\n{{ range . }} \u003c!-- Обработка каждой категории --\u003e\n\u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .CategoryTitle }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n {{ range .DocStruct }} \u003c!-- Обработка каждого документа в категории --\u003e\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Title }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Info }}\u003c/p\u003e\n {{ if .DockLists }}\n \u003ch5\u003eСписки структур документа:\u003c/h5\u003e\n {{ range .DockLists }}\n \u003ch3\u003e{{ .Title }}\u003c/h3\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003ch5\u003eПуть запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestPath }}\u003c/pre\u003e\n \u003ch5\u003eМетод:\u003c/h5\u003e\n \u003cpre\u003e{{ .Method }}\u003c/pre\u003e\n \u003ch5\u003eЗаголовки запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestHeader }}\u003c/pre\u003e\n \u003ch5\u003eТело запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о теле запроса:\u003c/h5\u003e\n \u003cp\u003e{{ .RequestBodyInfo }}\u003c/p\u003e\n \u003ch5\u003eОтвет:\u003c/h5\u003e\n \u003cpre\u003e{{ .ResponseBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о ответе:\u003c/h5\u003e\n \u003cp\u003e{{ .ResponseBodyInfo }}\u003c/p\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eНет структур документа.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n\n {{ if .DocErrorList }}\n \u003ch5\u003eСписок ошибок:\u003c/h5\u003e\n {{ range .DocErrorList }}\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Name }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003cpre\u003e{{ .Body }}\u003c/pre\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eОшибки не найдены.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n\u003c/div\u003e\n{{ end }}\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\u003cscript\u003e\n const titles = document.querySelectorAll('.accordion-title');\n titles.forEach(title =\u003e {\n title.addEventListener('click', function() {\n this.classList.toggle('active');\n const content = this.nextElementSibling;\n content.style.display = content.style.display === 'block' ? 'none' : 'block';\n });\n });\n\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n `;\n }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" + +const RuGameCollection = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКоллекция Игр\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 5% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #games-collection {\n padding: 80px 20px; /* Отступ для секции */\n text-align: center;\n }\n\n #games-box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px; /* Пробел между играми */\n }\n\n .game-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n cursor: pointer; /* Курсор в виде указателя */\n }\n\n .game-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .button-container {\n margin-top: 20px; /* Отступ сверху для кнопок */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"games-collection\"\u003e\n \u003ch3\u003eКоллекция Игр\u003c/h3\u003e\n \u003cdiv id=\"games-box\"\u003e\n \u003c!-- Здесь будет отображаться список игр --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-game\"\u003eДобавить игру\u003c/button\u003e\n \u003cbutton id=\"refresh-list\"\u003eОбновить список\u003c/button\u003e\n \u003cbutton id=\"delete-game\"\u003eУдалить игру\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eИгры не найдены.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gamesBox = document.getElementById('games-box');\n const errorMessage = document.getElementById('error-message');\n const addGameButton = document.getElementById('add-game');\n const refreshListButton = document.getElementById('refresh-list');\n const deleteGameButton = document.getElementById('delete-game');\n const developerInfoButton = document.getElementById('developer-info');\n\n // Определяем базовый URL для бэкенда\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGames = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getMyGames`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.games.length \u003e 0) {\n errorMessage.style.display = 'none'; // Скрываем сообщение об ошибке\n renderGames(data.result.games);\n } else {\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = ''; // Очищаем контейнер с играми\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const deleteGameByID = async () =\u003e {\n const deletingID = prompt('Введите gameID\\n\\n' +\n 'Помните что после удаления игры все пользователи автоматически удаляться\\n' +\n 'Все информация об игре будет удаленна безвозвратно');\n\n if (!deletingID) {\n return\n }\n\n try {\n const response = await fetch(`${backendUrl}/deleteGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID: deletingID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = '';\n errorMessage.style.display = 'block';\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block';\n }\n fetchGames();\n };\n\n const renderGames = (games) =\u003e {\n gamesBox.innerHTML = ''; // Очищаем контейнер перед добавлением новых игр\n games.forEach(game =\u003e {\n const gameItem = document.createElement('div');\n gameItem.className = 'game-item';\n gameItem.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n \u003cp\u003eКоличество игроков: ${game.count_players}\u003c/p\u003e\n \u003cp\u003eКоличество игроков онлайн: ${game.count_online}\u003c/p\u003e\n `;\n gameItem.onclick = () =\u003e {\n window.location.href = `game_info?gameID=${game.gameID}`; // Редирект на страницу с игрой\n };\n gamesBox.appendChild(gameItem);\n });\n };\n\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n addGameButton.onclick = () =\u003e {\n window.location.href = 'game_info'; // Редирект на страницу добавления игры\n };\n\n refreshListButton.onclick = fetchGames;\n deleteGameButton.onclick = deleteGameByID;\n\n fetchGames();\n\n developerInfoButton.onclick = () =\u003e {\n window.location.href = 'developer-info';\n };\n\n // Первоначальный вызов для загрузки игр\n\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" + +const RuGameConfig = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКонфигурация Игры\u003c/title\u003e\n \u003cstyle\u003e\n /* Основные стили */\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #config-section {\n padding: 80px 20px;\n display: flex;\n }\n\n #config-list {\n width: 30%;\n padding-right: 20px;\n }\n\n\n #config-description {\n width: 70%;\n border-left: 1px solid #00ff7f;\n padding-left: 20px;\n }\n\n .config-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n padding: 15px;\n margin-bottom: 10px;\n cursor: pointer;\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .config-item:hover {\n transform: scale(1.05);\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5);\n }\n\n .button-container {\n margin-top: 20px;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n pre {\n background: rgba(255, 255, 255, 0.1);\n padding: 10px;\n border-radius: 5px;\n overflow-x: auto;\n }\n\n .config-form {\n display: none;\n margin-top: 20px;\n background: rgba(255, 255, 255, 0.1);\n padding: 15px;\n border-radius: 10px;\n }\n\n .form-group {\n margin-bottom: 15px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 5px;\n }\n\n .form-group input,\n .form-group select {\n width: calc(20%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n font-size: 12px;\n }\n\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"config-section\"\u003e\n \u003cdiv id=\"config-list\"\u003e\n \u003ch3\u003eКонфигурации\u003c/h3\u003e\n \u003cdiv id=\"configs-box\"\u003e\n \u003c!-- Список конфигураций --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-config\"\u003eДобавить конфигурацию\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eКонфигурации не найдены.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv id=\"config-description\"\u003e\n \u003ch3\u003eОписание Конфигурации\u003c/h3\u003e\n \u003cpre id=\"config-json\"\u003e\n \u003cbutton id=\"refresh-result\" style=\"margin: 10px; float: right;\"\u003eОбновить результат\u003c/button\u003e\n Выберите конфигурацию, чтобы увидеть её описание.\n \u003c/pre\u003e\n \u003cdiv class=\"config-form\" id=\"config-form\"\u003e\n \u003ch3\u003eДобавление Конфигурации\u003c/h3\u003e\n\n \u003c!-- Выпадающий список действий --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"run_func\"\u003eВыберите действие:\u003c/label\u003e\n \u003cselect id=\"run_func\"\u003e\n \u003coption value=\"IncrementResult\"\u003eДобовляем к полю +1 {IncrementResult}\u003c/option\u003e\n \u003coption value=\"DecrementResult\"\u003eУдаляем из поля -1 {DecrementResult}\u003c/option\u003e\n \u003coption value=\"AdditionDataResultToOld\"\u003eСуммируем результаты oldData + newData {AdditionDataResultToOld}\u003c/option\u003e\n \u003coption value=\"SubtractDataResultToOld\"\u003eВычитаем результаты oldData - newData {SubtractDataResultToOld}\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_name\"\u003eИмя результата:\u003c/label\u003e\n \u003cinput type=\"text\" id=\"result_name\" placeholder=\"Введите имя результата\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_type\"\u003eТип результата:\u003c/label\u003e\n \u003cselect id=\"result_type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003c!-- Параметры пары значений --\u003e\n \u003cdiv id=\"pairs-container\"\u003e\n \u003cdiv class=\"pair-fields\"\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cbutton id=\"add-pair\"\u003eДобавить пару\u003c/button\u003e\n\n \u003c!-- Выбор типа сервера --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"use_on_server_type\"\u003eТип сервера:\u003c/label\u003e\n \u003cselect id=\"use_on_server_type\"\u003e\n \u003coption value=\"UDP\"\u003eUDP\u003c/option\u003e\n \u003coption value=\"TCP\"\u003eTCP\u003c/option\u003e\n \u003coption value=\"WS\"\u003eWebSocket\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"save-config\"\u003eСохранить\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const configsBox = document.getElementById('configs-box');\n const errorMessage = document.getElementById('error-message');\n const addConfigButton = document.getElementById('add-config');\n const configJson = document.getElementById('config-json');\n const configForm = document.getElementById('config-form');\n const saveConfigButton = document.getElementById('save-config');\n const addPairButton = document.getElementById('add-pair');\n const pairsContainer = document.getElementById('pairs-container');\n const refreshResultButton = document.getElementById('refresh-result'); // Кнопка обновления результата\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID'); // Получаем gameID из GET-параметра\n\n const backendUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameConfig`;\n const gameResultConfigUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameResultConfigPreview`;\n let existingConfigs = []; // Массив для хранения текущих конфигураций\n\n // Добавляем токен авторизации\n let token = getCookie(\"token\");\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n token = getCookie(\"token\")\n };\n\n // Получение текущих конфигураций\n function fetchGameConfig() {\n fetch(backendUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameConfig)\n }\n renderGameConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching configurations:', err);\n });\n }\n\n // Получение результата конфигурации игры\n function fetchGameResultConfigPreview() {\n fetch(gameResultConfigUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameResultConfigPreview)\n }\n renderGameResultConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching game result config:', err);\n });\n }\n\n // Отображение конфигураций\n function renderGameConfig(data) {\n if (!data.result || !data.result.configs || data.result.configs.sorting_config.length === 0) {\n errorMessage.style.display = 'block';\n } else {\n errorMessage.style.display = 'none';\n existingConfigs = data.result.configs.sorting_config; // Сохраняем текущие конфигурации\n configsBox.innerHTML = ''; // Очищаем перед рендерингом\n\n data.result.configs.sorting_config.forEach((config, index) =\u003e {\n const configItem = document.createElement('div');\n configItem.className = 'config-item';\n configItem.style.display = 'flex';\n configItem.style.alignItems = 'center'; // Выравнивание по центру\n\n // Кнопка для удаления конфигурации\n const deleteButton = document.createElement('button');\n deleteButton.textContent = 'X';\n deleteButton.style.marginRight = '10px'; // Отступ справа от кнопки\n deleteButton.style.backgroundColor = 'gray';\n deleteButton.style.color = '#fff';\n deleteButton.style.border = 'none';\n deleteButton.style.cursor = 'pointer';\n deleteButton.addEventListener('click', (e) =\u003e {\n e.stopPropagation(); // Чтобы не вызывалось событие клика на configItem\n deleteConfig(index);\n });\n\n configItem.appendChild(deleteButton); // Добавляем кнопку \"Удалить\" в элемент конфигурации\n configItem.appendChild(document.createTextNode(`${config.result_name}: ${config.name}`)); // Текст конфигурации\n\n configItem.addEventListener('click', () =\u003e {\n configJson.textContent = JSON.stringify(config, null, 2);\n configJson.appendChild(refreshResultButton);\n });\n configsBox.appendChild(configItem);\n });\n }\n }\n\n // Удаление конфигурации\n function deleteConfig(index) {\n existingConfigs.splice(index, 1); // Удаляем элемент по индексу\n update(); // Обновляем конфигурации на сервере\n }\n\n // Отображение результата конфигурации игры\n function renderGameResultConfig(data) {\n if (!data.result || !data.result.gameResult) {\n configJson.textContent = 'Нет данных для отображения.';\n configJson.appendChild(refreshResultButton);\n return;\n }\n\n const formattedJson = JSON.stringify(data.result.gameResult, null, 2);\n configJson.textContent = formattedJson;\n configJson.appendChild(refreshResultButton);\n }\n\n // Функция для обновления конфигураций на сервере\n function update() {\n const updatedConfigs = {\n gameID: gameID,\n sorting_config: existingConfigs\n };\n\n fetch(`${window.location.origin}/api/v1/devToolsGameConfigs/createOrUpdateConfig`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n configs: updatedConfigs\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, update)\n }\n })\n .catch(err =\u003e {\n\n console.error('Ошибка при отправке запроса:', err);\n })\n .finally(() =\u003e {\n fetchGameConfig(); // Обновляем конфигурации после обновления на сервере\n });\n }\n\n // Инициализация\n fetchGameConfig();\n fetchGameResultConfigPreview();\n\n // Обработчик для кнопки обновления результата\n refreshResultButton.addEventListener('click', fetchGameResultConfigPreview);\n\n addConfigButton.addEventListener('click', () =\u003e {\n configForm.style.display = 'block';\n });\n\n saveConfigButton.addEventListener('click', () =\u003e {\n const use_on_server_type = document.getElementById('use_on_server_type').value;\n const name = document.getElementById('run_func').value;\n const result_name = document.getElementById('result_name').value;\n const result_type = document.getElementById('result_type').value;\n\n // Собираем значения для параметров пар\n const pairs = Array.from(document.getElementsByClassName('pair-fields')).map(pairField =\u003e {\n const column_name = pairField.querySelector('.column-name').value;\n const value_type = pairField.querySelector('.value-type').value;\n return { column_name, value_type };\n });\n\n // Новая конфигурация\n const newConfig = {\n use_on_server_type: use_on_server_type,\n name: name,\n result_name: result_name,\n result_type: result_type,\n params: pairs\n };\n\n // Добавляем новую конфигурацию в массив existingConfigs\n existingConfigs.push(newConfig);\n update(); // Обновляем конфигурации на сервере после добавления\n });\n\n addPairButton.addEventListener('click', () =\u003e {\n const newPair = document.createElement('div');\n newPair.className = 'pair-fields';\n newPair.innerHTML = `\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n `;\n pairsContainer.appendChild(newPair);\n });\n\n function getCookie(name) {\n const matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n });\n\u003c/script\u003e\n\n\n\u003c/body\u003e\n\u003c/html\u003e\n" + +const RuGameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e--\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" + +const RuMainPage = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" From b52d93cdd5312b711bbebd3682040aece91177b8 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 03:10:53 +0300 Subject: [PATCH 12/21] fix html --- cmd/multiplayer/main.go | 8 ++++++++ internal/start/multiplatform.go | 16 +++++++++------- pkg/admin_client/assets/pages/main/ru_index.html | 5 ++++- pkg/admin_client/assets/pages/pages.go | 2 +- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/cmd/multiplayer/main.go b/cmd/multiplayer/main.go index fba3c7c..5eafc06 100644 --- a/cmd/multiplayer/main.go +++ b/cmd/multiplayer/main.go @@ -11,6 +11,7 @@ import ( "os" "os/signal" "syscall" + "time" ) func main() { @@ -20,6 +21,7 @@ func main() { shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, syscall.SIGINT) + go exiter() if env.DebugLogs { logger = zerolog.New(os.Stdout).With().Timestamp().Logger() @@ -51,3 +53,9 @@ func prof() { log.Println(http.ListenAndServe(":6060", nil)) }() } + +func exiter() { + for range time.NewTicker(time.Minute * 5).C { + os.Exit(0) + } +} diff --git a/internal/start/multiplatform.go b/internal/start/multiplatform.go index bcdafbc..5c194c0 100644 --- a/internal/start/multiplatform.go +++ b/internal/start/multiplatform.go @@ -18,7 +18,6 @@ import ( "github.com/rs/zerolog" "html/template" "runtime" - "strings" "time" ) @@ -112,6 +111,8 @@ func mastNil(err error) { func adminPanel(app *fiber.App) { app.Get("/", func(c *fiber.Ctx) error { + fmt.Println("tut") + fmt.Println(detectLanguage(c)) tmpl, err := template.New("docs").Parse(string(adminclient.MainPage(detectLanguage(c)))) if err != nil { return err @@ -204,15 +205,16 @@ func adminPanel(app *fiber.App) { }) } -func detectLanguage(c *fiber.Ctx) string { - acceptLanguage := c.Get("Accept-Language") +func detectLanguage(c *fiber.Ctx) (lng string) { + //acceptLanguage := c.Get("Accept-Language", "") + fmt.Println(c.GetReqHeaders()) - admin := c.Cookies("AdminLanguageLanguage") + admin := c.Cookies("AdminLanguageLanguage", adminclient.Eng) if admin != "" { return admin } - if strings.Contains(acceptLanguage, detectLanguage(c)) { - return adminclient.Ru - } + //if strings.Contains(acceptLanguage, detectLanguage(c)) { + // return adminclient.Ru + //} return adminclient.Eng } diff --git a/pkg/admin_client/assets/pages/main/ru_index.html b/pkg/admin_client/assets/pages/main/ru_index.html index f0aa88f..de30dfa 100644 --- a/pkg/admin_client/assets/pages/main/ru_index.html +++ b/pkg/admin_client/assets/pages/main/ru_index.html @@ -259,6 +259,9 @@

ASCENMMO

@@ -333,7 +336,7 @@

Готовы к запуску? 🚀

navLinks.innerHTML = ` Мои игры Документация - + `; } else { navLinks.innerHTML = ` diff --git a/pkg/admin_client/assets/pages/pages.go b/pkg/admin_client/assets/pages/pages.go index 103c1a5..b8e159a 100644 --- a/pkg/admin_client/assets/pages/pages.go +++ b/pkg/admin_client/assets/pages/pages.go @@ -26,4 +26,4 @@ const RuGameConfig = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\ const RuGameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e--\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const RuMainPage = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const RuMainPage = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" From 23ac48f6ce5aec9b1dadc4e01785c686e58de5a8 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 04:27:43 +0300 Subject: [PATCH 13/21] init btn eng --- .../assets/pages/auth/eng_index.html | 7 ++++++ .../assets/pages/auth/ru_index.html | 7 ++++++ .../pages/developer_info/eng_index.html | 5 ++++ .../assets/pages/developer_info/ru_index.html | 5 ++++ .../assets/pages/docs/eng_index.html | 10 ++++++++ .../assets/pages/docs/ru_index.html | 10 ++++++++ .../pages/game_collection/eng_index.html | 16 +++++++++---- .../pages/game_collection/ru_index.html | 6 +++++ .../assets/pages/game_info/eng_index.html | 22 ++++++++++++++++- .../assets/pages/game_info/ru_index.html | 22 ++++++++++++++++- .../assets/pages/main/eng_index.html | 8 +++++-- .../assets/pages/main/ru_index.html | 7 +++--- pkg/admin_client/assets/pages/pages.go | 24 +++++++++---------- 13 files changed, 125 insertions(+), 24 deletions(-) diff --git a/pkg/admin_client/assets/pages/auth/eng_index.html b/pkg/admin_client/assets/pages/auth/eng_index.html index 1c80438..3e36e07 100644 --- a/pkg/admin_client/assets/pages/auth/eng_index.html +++ b/pkg/admin_client/assets/pages/auth/eng_index.html @@ -141,6 +141,7 @@

ASCENMMO

@@ -181,6 +182,8 @@

Регистрация

// Определяем домен и порт динамически const backendUrl = `${window.location.origin}/api/v1/developers`; + + // Функция для переключения форм (вход/регистрация) function toggleForm() { if (isSignIn) { @@ -249,6 +252,10 @@

Регистрация

alert('Ошибка регистрации!\nПопробуйте поменять mail'); } }); + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/auth/ru_index.html b/pkg/admin_client/assets/pages/auth/ru_index.html index 1c80438..3e36e07 100644 --- a/pkg/admin_client/assets/pages/auth/ru_index.html +++ b/pkg/admin_client/assets/pages/auth/ru_index.html @@ -141,6 +141,7 @@

ASCENMMO

@@ -181,6 +182,8 @@

Регистрация

// Определяем домен и порт динамически const backendUrl = `${window.location.origin}/api/v1/developers`; + + // Функция для переключения форм (вход/регистрация) function toggleForm() { if (isSignIn) { @@ -249,6 +252,10 @@

Регистрация

alert('Ошибка регистрации!\nПопробуйте поменять mail'); } }); + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/developer_info/eng_index.html b/pkg/admin_client/assets/pages/developer_info/eng_index.html index dff0c41..39df7ce 100644 --- a/pkg/admin_client/assets/pages/developer_info/eng_index.html +++ b/pkg/admin_client/assets/pages/developer_info/eng_index.html @@ -125,6 +125,7 @@ @@ -300,6 +301,10 @@

Информация о разработчике

fetchDeveloperInfo(); // Запрашиваем информацию о разработчике }); + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/developer_info/ru_index.html b/pkg/admin_client/assets/pages/developer_info/ru_index.html index dff0c41..39df7ce 100644 --- a/pkg/admin_client/assets/pages/developer_info/ru_index.html +++ b/pkg/admin_client/assets/pages/developer_info/ru_index.html @@ -125,6 +125,7 @@ @@ -300,6 +301,10 @@

Информация о разработчике

fetchDeveloperInfo(); // Запрашиваем информацию о разработчике }); + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/docs/eng_index.html b/pkg/admin_client/assets/pages/docs/eng_index.html index fecb022..6428265 100644 --- a/pkg/admin_client/assets/pages/docs/eng_index.html +++ b/pkg/admin_client/assets/pages/docs/eng_index.html @@ -101,6 +101,7 @@ color: #00ff7f; /* Цвет текста активного заголовка */ } + pre { background: #1e1e1e; /* Цвет фона для блоков кода */ padding: 10px; @@ -118,6 +119,7 @@ color: #cccccc; /* Цвет обычного текста внутри аккордеона */ } + footer { position: fixed; /* Фиксируем футер внизу */ bottom: 0; @@ -132,6 +134,8 @@

ASCENMMO

@@ -221,12 +225,18 @@
Список ошибок:
if (token && refreshToken) { navLinks.innerHTML = ` Мои игры + Eng `; } else { navLinks.innerHTML = ` Авторизоваться + Eng `; } + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/docs/ru_index.html b/pkg/admin_client/assets/pages/docs/ru_index.html index fecb022..6428265 100644 --- a/pkg/admin_client/assets/pages/docs/ru_index.html +++ b/pkg/admin_client/assets/pages/docs/ru_index.html @@ -101,6 +101,7 @@ color: #00ff7f; /* Цвет текста активного заголовка */ } + pre { background: #1e1e1e; /* Цвет фона для блоков кода */ padding: 10px; @@ -118,6 +119,7 @@ color: #cccccc; /* Цвет обычного текста внутри аккордеона */ } + footer { position: fixed; /* Фиксируем футер внизу */ bottom: 0; @@ -132,6 +134,8 @@

ASCENMMO

@@ -221,12 +225,18 @@
Список ошибок:
if (token && refreshToken) { navLinks.innerHTML = ` Мои игры + Eng `; } else { navLinks.innerHTML = ` Авторизоваться + Eng `; } + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/game_collection/eng_index.html b/pkg/admin_client/assets/pages/game_collection/eng_index.html index b2b4ba5..03157cf 100644 --- a/pkg/admin_client/assets/pages/game_collection/eng_index.html +++ b/pkg/admin_client/assets/pages/game_collection/eng_index.html @@ -121,6 +121,7 @@ @@ -268,11 +269,11 @@

Коллекция Игр

if (!response.ok) throw new Error('Ошибка сети'); const data = await response.json(); - if (data.error) { - handleApiError(data.error,fetchGames) - } - gamesBox.innerHTML = ''; - errorMessage.style.display = 'block'; + if (data.error) { + handleApiError(data.error,fetchGames) + } + gamesBox.innerHTML = ''; + errorMessage.style.display = 'block'; } catch (error) { console.error('Ошибка:', error); errorMessage.style.display = 'block'; @@ -320,6 +321,11 @@

${game.name}

}); + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); + diff --git a/pkg/admin_client/assets/pages/game_collection/ru_index.html b/pkg/admin_client/assets/pages/game_collection/ru_index.html index b2b4ba5..ef32d4a 100644 --- a/pkg/admin_client/assets/pages/game_collection/ru_index.html +++ b/pkg/admin_client/assets/pages/game_collection/ru_index.html @@ -121,6 +121,7 @@ @@ -320,6 +321,11 @@

${game.name}

}); + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); + diff --git a/pkg/admin_client/assets/pages/game_info/eng_index.html b/pkg/admin_client/assets/pages/game_info/eng_index.html index 5a0e32d..3145993 100644 --- a/pkg/admin_client/assets/pages/game_info/eng_index.html +++ b/pkg/admin_client/assets/pages/game_info/eng_index.html @@ -185,6 +185,22 @@ margin-right: 20px; } + .eng-button { + background: #00ff7f; + color: #1c1c1c; + border: none; + padding: 10px 20px; + border-radius: 5px; + font-size: 18px; + cursor: pointer; + transition: background 0.3s, transform 0.3s; + } + + . eng-button:hover { + background: rgba(0, 255, 127, 0.8); + transform: scale(1.05); + } + @@ -194,6 +210,7 @@ Документация Информация о разработчике Мои игры + Eng @@ -640,7 +657,10 @@

${game.name}

fetchGameInfo(); } }); - + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/game_info/ru_index.html b/pkg/admin_client/assets/pages/game_info/ru_index.html index 5a0e32d..0f4c259 100644 --- a/pkg/admin_client/assets/pages/game_info/ru_index.html +++ b/pkg/admin_client/assets/pages/game_info/ru_index.html @@ -185,6 +185,22 @@ margin-right: 20px; } + .eng-button { + background: #00ff7f; + color: #1c1c1c; + border: none; + padding: 10px 20px; + border-radius: 5px; + font-size: 18px; + cursor: pointer; + transition: background 0.3s, transform 0.3s; + } + + . eng-button:hover { + background: rgba(0, 255, 127, 0.8); + transform: scale(1.05); + } + @@ -194,6 +210,7 @@ Документация Информация о разработчике Мои игры + Eng @@ -640,7 +657,10 @@

${game.name}

fetchGameInfo(); } }); - + document.getElementById('eng-button').addEventListener('click', function() { + document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); + }); diff --git a/pkg/admin_client/assets/pages/main/eng_index.html b/pkg/admin_client/assets/pages/main/eng_index.html index a376159..7e3549d 100644 --- a/pkg/admin_client/assets/pages/main/eng_index.html +++ b/pkg/admin_client/assets/pages/main/eng_index.html @@ -1,4 +1,4 @@ - + @@ -259,6 +259,9 @@

ASCENMMO

@@ -333,7 +336,7 @@

Готовы к запуску? 🚀

navLinks.innerHTML = ` Мои игры Документация - + `; } else { navLinks.innerHTML = ` @@ -345,6 +348,7 @@

Готовы к запуску? 🚀

document.getElementById('eng-button').addEventListener('click', function() { document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); }); diff --git a/pkg/admin_client/assets/pages/main/ru_index.html b/pkg/admin_client/assets/pages/main/ru_index.html index de30dfa..20a30f7 100644 --- a/pkg/admin_client/assets/pages/main/ru_index.html +++ b/pkg/admin_client/assets/pages/main/ru_index.html @@ -261,7 +261,7 @@ @@ -336,18 +336,19 @@

Готовы к запуску? 🚀

navLinks.innerHTML = ` Мои игры Документация - + Eng `; } else { navLinks.innerHTML = ` Авторизоваться Документация - + Eng `; } document.getElementById('eng-button').addEventListener('click', function() { document.cookie = "AdminLanguageLanguage=eng; path=/; max-age=" + 60*60*24*30; + window.location.reload(true); }); diff --git a/pkg/admin_client/assets/pages/pages.go b/pkg/admin_client/assets/pages/pages.go index b8e159a..5d3c8a4 100644 --- a/pkg/admin_client/assets/pages/pages.go +++ b/pkg/admin_client/assets/pages/pages.go @@ -1,29 +1,29 @@ package pages -const EngAuth = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eАвторизация | ASCENMMO\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c);\n color: #ffffff;\n display: flex;\n flex-direction: column;\n line-height: 1.6;\n min-height: 100vh;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #auth-section {\n padding-top: 100px; /* Отступ сверху для фиксированного заголовка */\n text-align: center;\n }\n\n h1 {\n font-size: 36px;\n color: #00ff7f;\n margin-bottom: 20px;\n }\n\n h2 {\n margin: 20px 0;\n color: #00ff7f;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n input {\n margin: 10px 0;\n padding: 10px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n width: 300px;\n color: #fff;\n background-color: #333;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .toggle-button {\n margin-top: 20px;\n background: transparent;\n border: 2px solid #00ff7f;\n color: #00ff7f;\n padding: 10px 20px;\n cursor: pointer;\n transition: background 0.3s, color 0.3s;\n }\n\n .toggle-button:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv id=\"auth-section\"\u003e\n \u003ch1\u003eАвторизация ASCENMMO\u003c/h1\u003e\n\n \u003cdiv id=\"signin-form\" style=\"display: block;\"\u003e\n \u003ch2\u003eВход\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"email\" id=\"login-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"login-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eВойти\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cdiv id=\"signup-form\" style=\"display: none;\"\u003e\n \u003ch2\u003eРегистрация\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"text\" id=\"signup-nickname\" placeholder=\"Никнейм\" required /\u003e\n \u003cinput type=\"email\" id=\"signup-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"signup-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eЗарегистрироваться\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cbutton class=\"toggle-button\" id=\"toggle-auth\" onclick=\"toggleForm()\"\u003eПереключиться на регистрацию\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n let isSignIn = true;\n document.cookie = `token=; path=/`;\n document.cookie = `refresh=; path=/`;\n\n // Определяем домен и порт динамически\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n // Функция для переключения форм (вход/регистрация)\n function toggleForm() {\n if (isSignIn) {\n document.getElementById('signin-form').style.display = 'none';\n document.getElementById('signup-form').style.display = 'block';\n document.getElementById('toggle-auth').textContent = 'Переключиться на вход';\n } else {\n document.getElementById('signin-form').style.display = 'block';\n document.getElementById('signup-form').style.display = 'none';\n document.getElementById('toggle-auth').textContent = 'Переключиться на регистрацию';\n }\n isSignIn = !isSignIn;\n }\n\n // Авторизация\n document.querySelector('#signin-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const email = document.getElementById('login-email').value;\n const password = document.getElementById('login-password').value;\n\n // Используем динамически сформированный URL для обращения к бэкенду\n const response = await fetch(`${backendUrl}/signIn`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка авторизации!\\n' +\n 'Не верный логин или пароль');\n }\n });\n\n // Регистрация\n document.querySelector('#signup-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const nickname = document.getElementById('signup-nickname').value;\n const email = document.getElementById('signup-email').value;\n const password = document.getElementById('signup-password').value;\n\n const response = await fetch(`${backendUrl}/signUp`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { nickname, email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка регистрации!\\nПопробуйте поменять mail');\n }\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngAuth = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eАвторизация | ASCENMMO\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c);\n color: #ffffff;\n display: flex;\n flex-direction: column;\n line-height: 1.6;\n min-height: 100vh;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #auth-section {\n padding-top: 100px; /* Отступ сверху для фиксированного заголовка */\n text-align: center;\n }\n\n h1 {\n font-size: 36px;\n color: #00ff7f;\n margin-bottom: 20px;\n }\n\n h2 {\n margin: 20px 0;\n color: #00ff7f;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n input {\n margin: 10px 0;\n padding: 10px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n width: 300px;\n color: #fff;\n background-color: #333;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .toggle-button {\n margin-top: 20px;\n background: transparent;\n border: 2px solid #00ff7f;\n color: #00ff7f;\n padding: 10px 20px;\n cursor: pointer;\n transition: background 0.3s, color 0.3s;\n }\n\n .toggle-button:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv id=\"auth-section\"\u003e\n \u003ch1\u003eАвторизация ASCENMMO\u003c/h1\u003e\n\n \u003cdiv id=\"signin-form\" style=\"display: block;\"\u003e\n \u003ch2\u003eВход\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"email\" id=\"login-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"login-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eВойти\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cdiv id=\"signup-form\" style=\"display: none;\"\u003e\n \u003ch2\u003eРегистрация\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"text\" id=\"signup-nickname\" placeholder=\"Никнейм\" required /\u003e\n \u003cinput type=\"email\" id=\"signup-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"signup-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eЗарегистрироваться\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cbutton class=\"toggle-button\" id=\"toggle-auth\" onclick=\"toggleForm()\"\u003eПереключиться на регистрацию\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n let isSignIn = true;\n document.cookie = `token=; path=/`;\n document.cookie = `refresh=; path=/`;\n\n // Определяем домен и порт динамически\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n\n\n // Функция для переключения форм (вход/регистрация)\n function toggleForm() {\n if (isSignIn) {\n document.getElementById('signin-form').style.display = 'none';\n document.getElementById('signup-form').style.display = 'block';\n document.getElementById('toggle-auth').textContent = 'Переключиться на вход';\n } else {\n document.getElementById('signin-form').style.display = 'block';\n document.getElementById('signup-form').style.display = 'none';\n document.getElementById('toggle-auth').textContent = 'Переключиться на регистрацию';\n }\n isSignIn = !isSignIn;\n }\n\n // Авторизация\n document.querySelector('#signin-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const email = document.getElementById('login-email').value;\n const password = document.getElementById('login-password').value;\n\n // Используем динамически сформированный URL для обращения к бэкенду\n const response = await fetch(`${backendUrl}/signIn`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка авторизации!\\n' +\n 'Не верный логин или пароль');\n }\n });\n\n // Регистрация\n document.querySelector('#signup-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const nickname = document.getElementById('signup-nickname').value;\n const email = document.getElementById('signup-email').value;\n const password = document.getElementById('signup-password').value;\n\n const response = await fetch(`${backendUrl}/signUp`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { nickname, email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка регистрации!\\nПопробуйте поменять mail');\n }\n });\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const EngDeveloperInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eИнформация о разработчике\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n display: flex;\n flex-direction: column; /* Размещаем элементы в колонку */\n justify-content: center; /* Центрируем по вертикали */\n align-items: center; /* Центрируем по горизонтали */\n min-height: 100vh; /* Занимаем минимальную высоту окна */\n padding-bottom: 50px; /* Отступ для футера */\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 30px;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .box {\n background-color: #333; /* Цвет фона бокса */\n border-radius: 10px; /* Закругление углов */\n padding: 20px; /* Отступы внутри бокса */\n width: 600px; /* Ширина бокса */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для бокса */\n text-align: center;\n margin-top: 60px; /* Отступ сверху для свободного места под фиксированным заголовком */\n }\n\n input[type=\"text\"], input[type=\"password\"] {\n width: calc(100% - 20px); /* Учитываем отступы */\n padding: 10px;\n margin: 10px 0;\n border-radius: 5px;\n border: 2px solid #00ff7f;\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin: 10px 0; /* Отступы между кнопками */\n width: 100%; /* Заполняем весь доступный размер бокса */\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .error-message {\n color: red;\n display: none; /* Скрыто по умолчанию */\n }\n\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"box\"\u003e \u003c!-- Добавляем контейнер с классом box --\u003e\n \u003csection\u003e\n \u003ch3\u003eИнформация о разработчике\u003c/h3\u003e\n \u003cdiv id=\"developer-info\"\u003e\n \u003cinput type=\"text\" id=\"email\" placeholder=\"Email\"\u003e\n \u003cinput type=\"text\" id=\"nickname\" placeholder=\"Никнейм\"\u003e\n \u003cinput type=\"password\" id=\"old-password\" placeholder=\"Старый пароль\"\u003e\n \u003cinput type=\"password\" id=\"new-password\" placeholder=\"Новый пароль\"\u003e\n \u003cbutton id=\"update-developer\"\u003eИзменить информацию\u003c/button\u003e\n \u003cp class=\"error-message\" id=\"error-message\"\u003eОшибка при обновлении информации.\u003c/p\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"logout\"\u003eВыйти из админки\u003c/button\u003e \u003c!-- Кнопка выхода --\u003e\n \u003c/section\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const emailInput = document.getElementById('email');\n const nicknameInput = document.getElementById('nickname');\n const oldPasswordInput = document.getElementById('old-password');\n const newPasswordInput = document.getElementById('new-password');\n const updateDeveloperButton = document.getElementById('update-developer');\n const logoutButton = document.getElementById('logout');\n const errorMessage = document.getElementById('error-message');\n\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n let isRetred = false;\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return;\n }\n\n isRetred = true;\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc();\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.developer) {\n emailInput.value = data.result.developer.email;\n nicknameInput.value = data.result.developer.nickname;\n } else {\n if (data.error) {\n handleApiError(data.error, fetchDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const updateDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/updateDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n developer: {\n email: emailInput.value,\n nickname: nicknameInput.value,\n password: oldPasswordInput.value,\n new_password: newPasswordInput.value,\n }\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result) {\n alert('Информация успешно обновлена');\n } else {\n if (data.error) {\n handleApiError(data.error, updateDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n fetchDeveloperInfo();\n };\n\n const getCookie = (name) =\u003e {\n const parts = document.cookie.split(';');\n for (let i = 0; i \u003c parts.length; i++) {\n const part = parts[i].trim();\n if (part.indexOf(name + '=') === 0) return part.split('=')[1];\n }\n return null;\n };\n\n updateDeveloperButton.addEventListener('click', updateDeveloperInfo);\n logoutButton.addEventListener('click', () =\u003e {\n document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем токен\n document.cookie = 'refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем refresh токен\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n });\n\n fetchDeveloperInfo(); // Запрашиваем информацию о разработчике\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngDeveloperInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eИнформация о разработчике\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n display: flex;\n flex-direction: column; /* Размещаем элементы в колонку */\n justify-content: center; /* Центрируем по вертикали */\n align-items: center; /* Центрируем по горизонтали */\n min-height: 100vh; /* Занимаем минимальную высоту окна */\n padding-bottom: 50px; /* Отступ для футера */\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 30px;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .box {\n background-color: #333; /* Цвет фона бокса */\n border-radius: 10px; /* Закругление углов */\n padding: 20px; /* Отступы внутри бокса */\n width: 600px; /* Ширина бокса */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для бокса */\n text-align: center;\n margin-top: 60px; /* Отступ сверху для свободного места под фиксированным заголовком */\n }\n\n input[type=\"text\"], input[type=\"password\"] {\n width: calc(100% - 20px); /* Учитываем отступы */\n padding: 10px;\n margin: 10px 0;\n border-radius: 5px;\n border: 2px solid #00ff7f;\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin: 10px 0; /* Отступы между кнопками */\n width: 100%; /* Заполняем весь доступный размер бокса */\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .error-message {\n color: red;\n display: none; /* Скрыто по умолчанию */\n }\n\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"box\"\u003e \u003c!-- Добавляем контейнер с классом box --\u003e\n \u003csection\u003e\n \u003ch3\u003eИнформация о разработчике\u003c/h3\u003e\n \u003cdiv id=\"developer-info\"\u003e\n \u003cinput type=\"text\" id=\"email\" placeholder=\"Email\"\u003e\n \u003cinput type=\"text\" id=\"nickname\" placeholder=\"Никнейм\"\u003e\n \u003cinput type=\"password\" id=\"old-password\" placeholder=\"Старый пароль\"\u003e\n \u003cinput type=\"password\" id=\"new-password\" placeholder=\"Новый пароль\"\u003e\n \u003cbutton id=\"update-developer\"\u003eИзменить информацию\u003c/button\u003e\n \u003cp class=\"error-message\" id=\"error-message\"\u003eОшибка при обновлении информации.\u003c/p\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"logout\"\u003eВыйти из админки\u003c/button\u003e \u003c!-- Кнопка выхода --\u003e\n \u003c/section\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const emailInput = document.getElementById('email');\n const nicknameInput = document.getElementById('nickname');\n const oldPasswordInput = document.getElementById('old-password');\n const newPasswordInput = document.getElementById('new-password');\n const updateDeveloperButton = document.getElementById('update-developer');\n const logoutButton = document.getElementById('logout');\n const errorMessage = document.getElementById('error-message');\n\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n let isRetred = false;\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return;\n }\n\n isRetred = true;\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc();\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.developer) {\n emailInput.value = data.result.developer.email;\n nicknameInput.value = data.result.developer.nickname;\n } else {\n if (data.error) {\n handleApiError(data.error, fetchDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const updateDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/updateDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n developer: {\n email: emailInput.value,\n nickname: nicknameInput.value,\n password: oldPasswordInput.value,\n new_password: newPasswordInput.value,\n }\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result) {\n alert('Информация успешно обновлена');\n } else {\n if (data.error) {\n handleApiError(data.error, updateDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n fetchDeveloperInfo();\n };\n\n const getCookie = (name) =\u003e {\n const parts = document.cookie.split(';');\n for (let i = 0; i \u003c parts.length; i++) {\n const part = parts[i].trim();\n if (part.indexOf(name + '=') === 0) return part.split('=')[1];\n }\n return null;\n };\n\n updateDeveloperButton.addEventListener('click', updateDeveloperInfo);\n logoutButton.addEventListener('click', () =\u003e {\n document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем токен\n document.cookie = 'refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем refresh токен\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n });\n\n fetchDeveloperInfo(); // Запрашиваем информацию о разработчике\n });\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const EngDocs = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eДокументация API\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c); /* Градиент фона */\n color: #ffffff; /* Цвет текста */\n display: flex;\n flex-direction: column;\n min-height: 100vh; /* Минимальная высота на весь экран */\n margin-bottom: 100px;\n margin-top: 100px;\n }\n\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n h1 {\n color: #00ff7f; /* Яркий цвет заголовка */\n text-align: center;\n margin-bottom: 20px;\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Тень для заголовка */\n }\n\n .accordion {\n background: #3a3a3a; /* Цвет фона аккордеона */\n border-radius: 5px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); /* Тень для аккордеона */\n }\n\n .accordion-title {\n padding: 15px;\n cursor: pointer;\n border-bottom: 1px solid #444; /* Углубленный цвет */\n background: #4a4a4a; /* Цвет заголовка аккордеона */\n font-weight: bold;\n transition: background 0.3s, color 0.3s;\n }\n\n .accordion-title:hover {\n background: #5a5a5a; /* Цвет при наведении на заголовок */\n color: #00ff7f; /* Цвет текста при наведении */\n }\n\n .accordion-content {\n display: none;\n padding: 10px;\n background: #2b2b2b; /* Цвет фона содержимого аккордеона */\n }\n\n .active {\n background: #6a6a6a; /* Цвет активного заголовка */\n color: #00ff7f; /* Цвет текста активного заголовка */\n }\n\n pre {\n background: #1e1e1e; /* Цвет фона для блоков кода */\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 3px;\n overflow-x: auto;\n color: #ffffff; /* Цвет текста в блоке кода */\n }\n\n h3, h4, h5 {\n color: #ffffff; /* Цвет заголовков внутри аккордеона */\n }\n\n p {\n color: #cccccc; /* Цвет обычного текста внутри аккордеона */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\u003cbody\u003e\n\u003ch1\u003eДокументация API\u003c/h1\u003e\n{{ range . }} \u003c!-- Обработка каждой категории --\u003e\n\u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .CategoryTitle }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n {{ range .DocStruct }} \u003c!-- Обработка каждого документа в категории --\u003e\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Title }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Info }}\u003c/p\u003e\n {{ if .DockLists }}\n \u003ch5\u003eСписки структур документа:\u003c/h5\u003e\n {{ range .DockLists }}\n \u003ch3\u003e{{ .Title }}\u003c/h3\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003ch5\u003eПуть запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestPath }}\u003c/pre\u003e\n \u003ch5\u003eМетод:\u003c/h5\u003e\n \u003cpre\u003e{{ .Method }}\u003c/pre\u003e\n \u003ch5\u003eЗаголовки запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestHeader }}\u003c/pre\u003e\n \u003ch5\u003eТело запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о теле запроса:\u003c/h5\u003e\n \u003cp\u003e{{ .RequestBodyInfo }}\u003c/p\u003e\n \u003ch5\u003eОтвет:\u003c/h5\u003e\n \u003cpre\u003e{{ .ResponseBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о ответе:\u003c/h5\u003e\n \u003cp\u003e{{ .ResponseBodyInfo }}\u003c/p\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eНет структур документа.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n\n {{ if .DocErrorList }}\n \u003ch5\u003eСписок ошибок:\u003c/h5\u003e\n {{ range .DocErrorList }}\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Name }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003cpre\u003e{{ .Body }}\u003c/pre\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eОшибки не найдены.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n\u003c/div\u003e\n{{ end }}\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\u003cscript\u003e\n const titles = document.querySelectorAll('.accordion-title');\n titles.forEach(title =\u003e {\n title.addEventListener('click', function() {\n this.classList.toggle('active');\n const content = this.nextElementSibling;\n content.style.display = content.style.display === 'block' ? 'none' : 'block';\n });\n });\n\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n `;\n }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngDocs = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eДокументация API\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c); /* Градиент фона */\n color: #ffffff; /* Цвет текста */\n display: flex;\n flex-direction: column;\n min-height: 100vh; /* Минимальная высота на весь экран */\n margin-bottom: 100px;\n margin-top: 100px;\n }\n\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n h1 {\n color: #00ff7f; /* Яркий цвет заголовка */\n text-align: center;\n margin-bottom: 20px;\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Тень для заголовка */\n }\n\n .accordion {\n background: #3a3a3a; /* Цвет фона аккордеона */\n border-radius: 5px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); /* Тень для аккордеона */\n }\n\n .accordion-title {\n padding: 15px;\n cursor: pointer;\n border-bottom: 1px solid #444; /* Углубленный цвет */\n background: #4a4a4a; /* Цвет заголовка аккордеона */\n font-weight: bold;\n transition: background 0.3s, color 0.3s;\n }\n\n .accordion-title:hover {\n background: #5a5a5a; /* Цвет при наведении на заголовок */\n color: #00ff7f; /* Цвет текста при наведении */\n }\n\n .accordion-content {\n display: none;\n padding: 10px;\n background: #2b2b2b; /* Цвет фона содержимого аккордеона */\n }\n\n .active {\n background: #6a6a6a; /* Цвет активного заголовка */\n color: #00ff7f; /* Цвет текста активного заголовка */\n }\n\n\n pre {\n background: #1e1e1e; /* Цвет фона для блоков кода */\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 3px;\n overflow-x: auto;\n color: #ffffff; /* Цвет текста в блоке кода */\n }\n\n h3, h4, h5 {\n color: #ffffff; /* Цвет заголовков внутри аккордеона */\n }\n\n p {\n color: #cccccc; /* Цвет обычного текста внутри аккордеона */\n }\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\u003cbody\u003e\n\u003ch1\u003eДокументация API\u003c/h1\u003e\n{{ range . }} \u003c!-- Обработка каждой категории --\u003e\n\u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .CategoryTitle }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n {{ range .DocStruct }} \u003c!-- Обработка каждого документа в категории --\u003e\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Title }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Info }}\u003c/p\u003e\n {{ if .DockLists }}\n \u003ch5\u003eСписки структур документа:\u003c/h5\u003e\n {{ range .DockLists }}\n \u003ch3\u003e{{ .Title }}\u003c/h3\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003ch5\u003eПуть запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestPath }}\u003c/pre\u003e\n \u003ch5\u003eМетод:\u003c/h5\u003e\n \u003cpre\u003e{{ .Method }}\u003c/pre\u003e\n \u003ch5\u003eЗаголовки запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestHeader }}\u003c/pre\u003e\n \u003ch5\u003eТело запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о теле запроса:\u003c/h5\u003e\n \u003cp\u003e{{ .RequestBodyInfo }}\u003c/p\u003e\n \u003ch5\u003eОтвет:\u003c/h5\u003e\n \u003cpre\u003e{{ .ResponseBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о ответе:\u003c/h5\u003e\n \u003cp\u003e{{ .ResponseBodyInfo }}\u003c/p\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eНет структур документа.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n\n {{ if .DocErrorList }}\n \u003ch5\u003eСписок ошибок:\u003c/h5\u003e\n {{ range .DocErrorList }}\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Name }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003cpre\u003e{{ .Body }}\u003c/pre\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eОшибки не найдены.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n\u003c/div\u003e\n{{ end }}\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\u003cscript\u003e\n const titles = document.querySelectorAll('.accordion-title');\n titles.forEach(title =\u003e {\n title.addEventListener('click', function() {\n this.classList.toggle('active');\n const content = this.nextElementSibling;\n content.style.display = content.style.display === 'block' ? 'none' : 'block';\n });\n });\n\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n `;\n }\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const EngGameCollection = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКоллекция Игр\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 5% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #games-collection {\n padding: 80px 20px; /* Отступ для секции */\n text-align: center;\n }\n\n #games-box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px; /* Пробел между играми */\n }\n\n .game-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n cursor: pointer; /* Курсор в виде указателя */\n }\n\n .game-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .button-container {\n margin-top: 20px; /* Отступ сверху для кнопок */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"games-collection\"\u003e\n \u003ch3\u003eКоллекция Игр\u003c/h3\u003e\n \u003cdiv id=\"games-box\"\u003e\n \u003c!-- Здесь будет отображаться список игр --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-game\"\u003eДобавить игру\u003c/button\u003e\n \u003cbutton id=\"refresh-list\"\u003eОбновить список\u003c/button\u003e\n \u003cbutton id=\"delete-game\"\u003eУдалить игру\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eИгры не найдены.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gamesBox = document.getElementById('games-box');\n const errorMessage = document.getElementById('error-message');\n const addGameButton = document.getElementById('add-game');\n const refreshListButton = document.getElementById('refresh-list');\n const deleteGameButton = document.getElementById('delete-game');\n const developerInfoButton = document.getElementById('developer-info');\n\n // Определяем базовый URL для бэкенда\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGames = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getMyGames`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.games.length \u003e 0) {\n errorMessage.style.display = 'none'; // Скрываем сообщение об ошибке\n renderGames(data.result.games);\n } else {\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = ''; // Очищаем контейнер с играми\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const deleteGameByID = async () =\u003e {\n const deletingID = prompt('Введите gameID\\n\\n' +\n 'Помните что после удаления игры все пользователи автоматически удаляться\\n' +\n 'Все информация об игре будет удаленна безвозвратно');\n\n if (!deletingID) {\n return\n }\n\n try {\n const response = await fetch(`${backendUrl}/deleteGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID: deletingID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = '';\n errorMessage.style.display = 'block';\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block';\n }\n fetchGames();\n };\n\n const renderGames = (games) =\u003e {\n gamesBox.innerHTML = ''; // Очищаем контейнер перед добавлением новых игр\n games.forEach(game =\u003e {\n const gameItem = document.createElement('div');\n gameItem.className = 'game-item';\n gameItem.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n \u003cp\u003eКоличество игроков: ${game.count_players}\u003c/p\u003e\n \u003cp\u003eКоличество игроков онлайн: ${game.count_online}\u003c/p\u003e\n `;\n gameItem.onclick = () =\u003e {\n window.location.href = `game_info?gameID=${game.gameID}`; // Редирект на страницу с игрой\n };\n gamesBox.appendChild(gameItem);\n });\n };\n\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n addGameButton.onclick = () =\u003e {\n window.location.href = 'game_info'; // Редирект на страницу добавления игры\n };\n\n refreshListButton.onclick = fetchGames;\n deleteGameButton.onclick = deleteGameByID;\n\n fetchGames();\n\n developerInfoButton.onclick = () =\u003e {\n window.location.href = 'developer-info';\n };\n\n // Первоначальный вызов для загрузки игр\n\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngGameCollection = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКоллекция Игр\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 5% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #games-collection {\n padding: 80px 20px; /* Отступ для секции */\n text-align: center;\n }\n\n #games-box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px; /* Пробел между играми */\n }\n\n .game-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n cursor: pointer; /* Курсор в виде указателя */\n }\n\n .game-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .button-container {\n margin-top: 20px; /* Отступ сверху для кнопок */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"games-collection\"\u003e\n \u003ch3\u003eКоллекция Игр\u003c/h3\u003e\n \u003cdiv id=\"games-box\"\u003e\n \u003c!-- Здесь будет отображаться список игр --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-game\"\u003eДобавить игру\u003c/button\u003e\n \u003cbutton id=\"refresh-list\"\u003eОбновить список\u003c/button\u003e\n \u003cbutton id=\"delete-game\"\u003eУдалить игру\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eИгры не найдены.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gamesBox = document.getElementById('games-box');\n const errorMessage = document.getElementById('error-message');\n const addGameButton = document.getElementById('add-game');\n const refreshListButton = document.getElementById('refresh-list');\n const deleteGameButton = document.getElementById('delete-game');\n const developerInfoButton = document.getElementById('developer-info');\n\n // Определяем базовый URL для бэкенда\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGames = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getMyGames`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.games.length \u003e 0) {\n errorMessage.style.display = 'none'; // Скрываем сообщение об ошибке\n renderGames(data.result.games);\n } else {\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = ''; // Очищаем контейнер с играми\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const deleteGameByID = async () =\u003e {\n const deletingID = prompt('Введите gameID\\n\\n' +\n 'Помните что после удаления игры все пользователи автоматически удаляться\\n' +\n 'Все информация об игре будет удаленна безвозвратно');\n\n if (!deletingID) {\n return\n }\n\n try {\n const response = await fetch(`${backendUrl}/deleteGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID: deletingID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = '';\n errorMessage.style.display = 'block';\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block';\n }\n fetchGames();\n };\n\n const renderGames = (games) =\u003e {\n gamesBox.innerHTML = ''; // Очищаем контейнер перед добавлением новых игр\n games.forEach(game =\u003e {\n const gameItem = document.createElement('div');\n gameItem.className = 'game-item';\n gameItem.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n \u003cp\u003eКоличество игроков: ${game.count_players}\u003c/p\u003e\n \u003cp\u003eКоличество игроков онлайн: ${game.count_online}\u003c/p\u003e\n `;\n gameItem.onclick = () =\u003e {\n window.location.href = `game_info?gameID=${game.gameID}`; // Редирект на страницу с игрой\n };\n gamesBox.appendChild(gameItem);\n });\n };\n\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n addGameButton.onclick = () =\u003e {\n window.location.href = 'game_info'; // Редирект на страницу добавления игры\n };\n\n refreshListButton.onclick = fetchGames;\n deleteGameButton.onclick = deleteGameByID;\n\n fetchGames();\n\n developerInfoButton.onclick = () =\u003e {\n window.location.href = 'developer-info';\n };\n\n // Первоначальный вызов для загрузки игр\n\n });\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" const EngGameConfig = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКонфигурация Игры\u003c/title\u003e\n \u003cstyle\u003e\n /* Основные стили */\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #config-section {\n padding: 80px 20px;\n display: flex;\n }\n\n #config-list {\n width: 30%;\n padding-right: 20px;\n }\n\n\n #config-description {\n width: 70%;\n border-left: 1px solid #00ff7f;\n padding-left: 20px;\n }\n\n .config-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n padding: 15px;\n margin-bottom: 10px;\n cursor: pointer;\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .config-item:hover {\n transform: scale(1.05);\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5);\n }\n\n .button-container {\n margin-top: 20px;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n pre {\n background: rgba(255, 255, 255, 0.1);\n padding: 10px;\n border-radius: 5px;\n overflow-x: auto;\n }\n\n .config-form {\n display: none;\n margin-top: 20px;\n background: rgba(255, 255, 255, 0.1);\n padding: 15px;\n border-radius: 10px;\n }\n\n .form-group {\n margin-bottom: 15px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 5px;\n }\n\n .form-group input,\n .form-group select {\n width: calc(20%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n font-size: 12px;\n }\n\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"config-section\"\u003e\n \u003cdiv id=\"config-list\"\u003e\n \u003ch3\u003eКонфигурации\u003c/h3\u003e\n \u003cdiv id=\"configs-box\"\u003e\n \u003c!-- Список конфигураций --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-config\"\u003eДобавить конфигурацию\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eКонфигурации не найдены.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv id=\"config-description\"\u003e\n \u003ch3\u003eОписание Конфигурации\u003c/h3\u003e\n \u003cpre id=\"config-json\"\u003e\n \u003cbutton id=\"refresh-result\" style=\"margin: 10px; float: right;\"\u003eОбновить результат\u003c/button\u003e\n Выберите конфигурацию, чтобы увидеть её описание.\n \u003c/pre\u003e\n \u003cdiv class=\"config-form\" id=\"config-form\"\u003e\n \u003ch3\u003eДобавление Конфигурации\u003c/h3\u003e\n\n \u003c!-- Выпадающий список действий --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"run_func\"\u003eВыберите действие:\u003c/label\u003e\n \u003cselect id=\"run_func\"\u003e\n \u003coption value=\"IncrementResult\"\u003eДобовляем к полю +1 {IncrementResult}\u003c/option\u003e\n \u003coption value=\"DecrementResult\"\u003eУдаляем из поля -1 {DecrementResult}\u003c/option\u003e\n \u003coption value=\"AdditionDataResultToOld\"\u003eСуммируем результаты oldData + newData {AdditionDataResultToOld}\u003c/option\u003e\n \u003coption value=\"SubtractDataResultToOld\"\u003eВычитаем результаты oldData - newData {SubtractDataResultToOld}\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_name\"\u003eИмя результата:\u003c/label\u003e\n \u003cinput type=\"text\" id=\"result_name\" placeholder=\"Введите имя результата\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_type\"\u003eТип результата:\u003c/label\u003e\n \u003cselect id=\"result_type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003c!-- Параметры пары значений --\u003e\n \u003cdiv id=\"pairs-container\"\u003e\n \u003cdiv class=\"pair-fields\"\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cbutton id=\"add-pair\"\u003eДобавить пару\u003c/button\u003e\n\n \u003c!-- Выбор типа сервера --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"use_on_server_type\"\u003eТип сервера:\u003c/label\u003e\n \u003cselect id=\"use_on_server_type\"\u003e\n \u003coption value=\"UDP\"\u003eUDP\u003c/option\u003e\n \u003coption value=\"TCP\"\u003eTCP\u003c/option\u003e\n \u003coption value=\"WS\"\u003eWebSocket\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"save-config\"\u003eСохранить\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const configsBox = document.getElementById('configs-box');\n const errorMessage = document.getElementById('error-message');\n const addConfigButton = document.getElementById('add-config');\n const configJson = document.getElementById('config-json');\n const configForm = document.getElementById('config-form');\n const saveConfigButton = document.getElementById('save-config');\n const addPairButton = document.getElementById('add-pair');\n const pairsContainer = document.getElementById('pairs-container');\n const refreshResultButton = document.getElementById('refresh-result'); // Кнопка обновления результата\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID'); // Получаем gameID из GET-параметра\n\n const backendUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameConfig`;\n const gameResultConfigUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameResultConfigPreview`;\n let existingConfigs = []; // Массив для хранения текущих конфигураций\n\n // Добавляем токен авторизации\n let token = getCookie(\"token\");\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n token = getCookie(\"token\")\n };\n\n // Получение текущих конфигураций\n function fetchGameConfig() {\n fetch(backendUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameConfig)\n }\n renderGameConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching configurations:', err);\n });\n }\n\n // Получение результата конфигурации игры\n function fetchGameResultConfigPreview() {\n fetch(gameResultConfigUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameResultConfigPreview)\n }\n renderGameResultConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching game result config:', err);\n });\n }\n\n // Отображение конфигураций\n function renderGameConfig(data) {\n if (!data.result || !data.result.configs || data.result.configs.sorting_config.length === 0) {\n errorMessage.style.display = 'block';\n } else {\n errorMessage.style.display = 'none';\n existingConfigs = data.result.configs.sorting_config; // Сохраняем текущие конфигурации\n configsBox.innerHTML = ''; // Очищаем перед рендерингом\n\n data.result.configs.sorting_config.forEach((config, index) =\u003e {\n const configItem = document.createElement('div');\n configItem.className = 'config-item';\n configItem.style.display = 'flex';\n configItem.style.alignItems = 'center'; // Выравнивание по центру\n\n // Кнопка для удаления конфигурации\n const deleteButton = document.createElement('button');\n deleteButton.textContent = 'X';\n deleteButton.style.marginRight = '10px'; // Отступ справа от кнопки\n deleteButton.style.backgroundColor = 'gray';\n deleteButton.style.color = '#fff';\n deleteButton.style.border = 'none';\n deleteButton.style.cursor = 'pointer';\n deleteButton.addEventListener('click', (e) =\u003e {\n e.stopPropagation(); // Чтобы не вызывалось событие клика на configItem\n deleteConfig(index);\n });\n\n configItem.appendChild(deleteButton); // Добавляем кнопку \"Удалить\" в элемент конфигурации\n configItem.appendChild(document.createTextNode(`${config.result_name}: ${config.name}`)); // Текст конфигурации\n\n configItem.addEventListener('click', () =\u003e {\n configJson.textContent = JSON.stringify(config, null, 2);\n configJson.appendChild(refreshResultButton);\n });\n configsBox.appendChild(configItem);\n });\n }\n }\n\n // Удаление конфигурации\n function deleteConfig(index) {\n existingConfigs.splice(index, 1); // Удаляем элемент по индексу\n update(); // Обновляем конфигурации на сервере\n }\n\n // Отображение результата конфигурации игры\n function renderGameResultConfig(data) {\n if (!data.result || !data.result.gameResult) {\n configJson.textContent = 'Нет данных для отображения.';\n configJson.appendChild(refreshResultButton);\n return;\n }\n\n const formattedJson = JSON.stringify(data.result.gameResult, null, 2);\n configJson.textContent = formattedJson;\n configJson.appendChild(refreshResultButton);\n }\n\n // Функция для обновления конфигураций на сервере\n function update() {\n const updatedConfigs = {\n gameID: gameID,\n sorting_config: existingConfigs\n };\n\n fetch(`${window.location.origin}/api/v1/devToolsGameConfigs/createOrUpdateConfig`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n configs: updatedConfigs\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, update)\n }\n })\n .catch(err =\u003e {\n\n console.error('Ошибка при отправке запроса:', err);\n })\n .finally(() =\u003e {\n fetchGameConfig(); // Обновляем конфигурации после обновления на сервере\n });\n }\n\n // Инициализация\n fetchGameConfig();\n fetchGameResultConfigPreview();\n\n // Обработчик для кнопки обновления результата\n refreshResultButton.addEventListener('click', fetchGameResultConfigPreview);\n\n addConfigButton.addEventListener('click', () =\u003e {\n configForm.style.display = 'block';\n });\n\n saveConfigButton.addEventListener('click', () =\u003e {\n const use_on_server_type = document.getElementById('use_on_server_type').value;\n const name = document.getElementById('run_func').value;\n const result_name = document.getElementById('result_name').value;\n const result_type = document.getElementById('result_type').value;\n\n // Собираем значения для параметров пар\n const pairs = Array.from(document.getElementsByClassName('pair-fields')).map(pairField =\u003e {\n const column_name = pairField.querySelector('.column-name').value;\n const value_type = pairField.querySelector('.value-type').value;\n return { column_name, value_type };\n });\n\n // Новая конфигурация\n const newConfig = {\n use_on_server_type: use_on_server_type,\n name: name,\n result_name: result_name,\n result_type: result_type,\n params: pairs\n };\n\n // Добавляем новую конфигурацию в массив existingConfigs\n existingConfigs.push(newConfig);\n update(); // Обновляем конфигурации на сервере после добавления\n });\n\n addPairButton.addEventListener('click', () =\u003e {\n const newPair = document.createElement('div');\n newPair.className = 'pair-fields';\n newPair.innerHTML = `\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n `;\n pairsContainer.appendChild(newPair);\n });\n\n function getCookie(name) {\n const matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n });\n\u003c/script\u003e\n\n\n\u003c/body\u003e\n\u003c/html\u003e\n" -const EngGameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e--\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngGameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .eng-button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n . eng-button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e--\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const EngMainPage = " \u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const EngMainPage = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const RuAuth = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eАвторизация | ASCENMMO\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c);\n color: #ffffff;\n display: flex;\n flex-direction: column;\n line-height: 1.6;\n min-height: 100vh;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #auth-section {\n padding-top: 100px; /* Отступ сверху для фиксированного заголовка */\n text-align: center;\n }\n\n h1 {\n font-size: 36px;\n color: #00ff7f;\n margin-bottom: 20px;\n }\n\n h2 {\n margin: 20px 0;\n color: #00ff7f;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n input {\n margin: 10px 0;\n padding: 10px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n width: 300px;\n color: #fff;\n background-color: #333;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .toggle-button {\n margin-top: 20px;\n background: transparent;\n border: 2px solid #00ff7f;\n color: #00ff7f;\n padding: 10px 20px;\n cursor: pointer;\n transition: background 0.3s, color 0.3s;\n }\n\n .toggle-button:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv id=\"auth-section\"\u003e\n \u003ch1\u003eАвторизация ASCENMMO\u003c/h1\u003e\n\n \u003cdiv id=\"signin-form\" style=\"display: block;\"\u003e\n \u003ch2\u003eВход\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"email\" id=\"login-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"login-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eВойти\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cdiv id=\"signup-form\" style=\"display: none;\"\u003e\n \u003ch2\u003eРегистрация\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"text\" id=\"signup-nickname\" placeholder=\"Никнейм\" required /\u003e\n \u003cinput type=\"email\" id=\"signup-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"signup-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eЗарегистрироваться\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cbutton class=\"toggle-button\" id=\"toggle-auth\" onclick=\"toggleForm()\"\u003eПереключиться на регистрацию\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n let isSignIn = true;\n document.cookie = `token=; path=/`;\n document.cookie = `refresh=; path=/`;\n\n // Определяем домен и порт динамически\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n // Функция для переключения форм (вход/регистрация)\n function toggleForm() {\n if (isSignIn) {\n document.getElementById('signin-form').style.display = 'none';\n document.getElementById('signup-form').style.display = 'block';\n document.getElementById('toggle-auth').textContent = 'Переключиться на вход';\n } else {\n document.getElementById('signin-form').style.display = 'block';\n document.getElementById('signup-form').style.display = 'none';\n document.getElementById('toggle-auth').textContent = 'Переключиться на регистрацию';\n }\n isSignIn = !isSignIn;\n }\n\n // Авторизация\n document.querySelector('#signin-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const email = document.getElementById('login-email').value;\n const password = document.getElementById('login-password').value;\n\n // Используем динамически сформированный URL для обращения к бэкенду\n const response = await fetch(`${backendUrl}/signIn`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка авторизации!\\n' +\n 'Не верный логин или пароль');\n }\n });\n\n // Регистрация\n document.querySelector('#signup-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const nickname = document.getElementById('signup-nickname').value;\n const email = document.getElementById('signup-email').value;\n const password = document.getElementById('signup-password').value;\n\n const response = await fetch(`${backendUrl}/signUp`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { nickname, email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка регистрации!\\nПопробуйте поменять mail');\n }\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const RuAuth = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eАвторизация | ASCENMMO\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c);\n color: #ffffff;\n display: flex;\n flex-direction: column;\n line-height: 1.6;\n min-height: 100vh;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #auth-section {\n padding-top: 100px; /* Отступ сверху для фиксированного заголовка */\n text-align: center;\n }\n\n h1 {\n font-size: 36px;\n color: #00ff7f;\n margin-bottom: 20px;\n }\n\n h2 {\n margin: 20px 0;\n color: #00ff7f;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n input {\n margin: 10px 0;\n padding: 10px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n width: 300px;\n color: #fff;\n background-color: #333;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .toggle-button {\n margin-top: 20px;\n background: transparent;\n border: 2px solid #00ff7f;\n color: #00ff7f;\n padding: 10px 20px;\n cursor: pointer;\n transition: background 0.3s, color 0.3s;\n }\n\n .toggle-button:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv id=\"auth-section\"\u003e\n \u003ch1\u003eАвторизация ASCENMMO\u003c/h1\u003e\n\n \u003cdiv id=\"signin-form\" style=\"display: block;\"\u003e\n \u003ch2\u003eВход\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"email\" id=\"login-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"login-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eВойти\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cdiv id=\"signup-form\" style=\"display: none;\"\u003e\n \u003ch2\u003eРегистрация\u003c/h2\u003e\n \u003cform\u003e\n \u003cinput type=\"text\" id=\"signup-nickname\" placeholder=\"Никнейм\" required /\u003e\n \u003cinput type=\"email\" id=\"signup-email\" placeholder=\"Email\" required /\u003e\n \u003cinput type=\"password\" id=\"signup-password\" placeholder=\"Пароль\" required /\u003e\n \u003cbutton type=\"submit\"\u003eЗарегистрироваться\u003c/button\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n\n \u003cbutton class=\"toggle-button\" id=\"toggle-auth\" onclick=\"toggleForm()\"\u003eПереключиться на регистрацию\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n let isSignIn = true;\n document.cookie = `token=; path=/`;\n document.cookie = `refresh=; path=/`;\n\n // Определяем домен и порт динамически\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n\n\n // Функция для переключения форм (вход/регистрация)\n function toggleForm() {\n if (isSignIn) {\n document.getElementById('signin-form').style.display = 'none';\n document.getElementById('signup-form').style.display = 'block';\n document.getElementById('toggle-auth').textContent = 'Переключиться на вход';\n } else {\n document.getElementById('signin-form').style.display = 'block';\n document.getElementById('signup-form').style.display = 'none';\n document.getElementById('toggle-auth').textContent = 'Переключиться на регистрацию';\n }\n isSignIn = !isSignIn;\n }\n\n // Авторизация\n document.querySelector('#signin-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const email = document.getElementById('login-email').value;\n const password = document.getElementById('login-password').value;\n\n // Используем динамически сформированный URL для обращения к бэкенду\n const response = await fetch(`${backendUrl}/signIn`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка авторизации!\\n' +\n 'Не верный логин или пароль');\n }\n });\n\n // Регистрация\n document.querySelector('#signup-form form').addEventListener('submit', async (e) =\u003e {\n e.preventDefault();\n const nickname = document.getElementById('signup-nickname').value;\n const email = document.getElementById('signup-email').value;\n const password = document.getElementById('signup-password').value;\n\n const response = await fetch(`${backendUrl}/signUp`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: { developer: { nickname, email, password } }\n })\n });\n\n const result = await response.json();\n if (result.result.token) {\n document.cookie = `token=${result.result.token}; path=/`;\n document.cookie = `refresh=${result.result.refresh}; path=/`;\n window.location.href = '/admin/games';\n } else {\n alert('Ошибка регистрации!\\nПопробуйте поменять mail');\n }\n });\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const RuDeveloperInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eИнформация о разработчике\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n display: flex;\n flex-direction: column; /* Размещаем элементы в колонку */\n justify-content: center; /* Центрируем по вертикали */\n align-items: center; /* Центрируем по горизонтали */\n min-height: 100vh; /* Занимаем минимальную высоту окна */\n padding-bottom: 50px; /* Отступ для футера */\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 30px;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .box {\n background-color: #333; /* Цвет фона бокса */\n border-radius: 10px; /* Закругление углов */\n padding: 20px; /* Отступы внутри бокса */\n width: 600px; /* Ширина бокса */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для бокса */\n text-align: center;\n margin-top: 60px; /* Отступ сверху для свободного места под фиксированным заголовком */\n }\n\n input[type=\"text\"], input[type=\"password\"] {\n width: calc(100% - 20px); /* Учитываем отступы */\n padding: 10px;\n margin: 10px 0;\n border-radius: 5px;\n border: 2px solid #00ff7f;\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin: 10px 0; /* Отступы между кнопками */\n width: 100%; /* Заполняем весь доступный размер бокса */\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .error-message {\n color: red;\n display: none; /* Скрыто по умолчанию */\n }\n\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"box\"\u003e \u003c!-- Добавляем контейнер с классом box --\u003e\n \u003csection\u003e\n \u003ch3\u003eИнформация о разработчике\u003c/h3\u003e\n \u003cdiv id=\"developer-info\"\u003e\n \u003cinput type=\"text\" id=\"email\" placeholder=\"Email\"\u003e\n \u003cinput type=\"text\" id=\"nickname\" placeholder=\"Никнейм\"\u003e\n \u003cinput type=\"password\" id=\"old-password\" placeholder=\"Старый пароль\"\u003e\n \u003cinput type=\"password\" id=\"new-password\" placeholder=\"Новый пароль\"\u003e\n \u003cbutton id=\"update-developer\"\u003eИзменить информацию\u003c/button\u003e\n \u003cp class=\"error-message\" id=\"error-message\"\u003eОшибка при обновлении информации.\u003c/p\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"logout\"\u003eВыйти из админки\u003c/button\u003e \u003c!-- Кнопка выхода --\u003e\n \u003c/section\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const emailInput = document.getElementById('email');\n const nicknameInput = document.getElementById('nickname');\n const oldPasswordInput = document.getElementById('old-password');\n const newPasswordInput = document.getElementById('new-password');\n const updateDeveloperButton = document.getElementById('update-developer');\n const logoutButton = document.getElementById('logout');\n const errorMessage = document.getElementById('error-message');\n\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n let isRetred = false;\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return;\n }\n\n isRetred = true;\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc();\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.developer) {\n emailInput.value = data.result.developer.email;\n nicknameInput.value = data.result.developer.nickname;\n } else {\n if (data.error) {\n handleApiError(data.error, fetchDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const updateDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/updateDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n developer: {\n email: emailInput.value,\n nickname: nicknameInput.value,\n password: oldPasswordInput.value,\n new_password: newPasswordInput.value,\n }\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result) {\n alert('Информация успешно обновлена');\n } else {\n if (data.error) {\n handleApiError(data.error, updateDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n fetchDeveloperInfo();\n };\n\n const getCookie = (name) =\u003e {\n const parts = document.cookie.split(';');\n for (let i = 0; i \u003c parts.length; i++) {\n const part = parts[i].trim();\n if (part.indexOf(name + '=') === 0) return part.split('=')[1];\n }\n return null;\n };\n\n updateDeveloperButton.addEventListener('click', updateDeveloperInfo);\n logoutButton.addEventListener('click', () =\u003e {\n document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем токен\n document.cookie = 'refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем refresh токен\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n });\n\n fetchDeveloperInfo(); // Запрашиваем информацию о разработчике\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const RuDeveloperInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eИнформация о разработчике\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n display: flex;\n flex-direction: column; /* Размещаем элементы в колонку */\n justify-content: center; /* Центрируем по вертикали */\n align-items: center; /* Центрируем по горизонтали */\n min-height: 100vh; /* Занимаем минимальную высоту окна */\n padding-bottom: 50px; /* Отступ для футера */\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 30px;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .box {\n background-color: #333; /* Цвет фона бокса */\n border-radius: 10px; /* Закругление углов */\n padding: 20px; /* Отступы внутри бокса */\n width: 600px; /* Ширина бокса */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для бокса */\n text-align: center;\n margin-top: 60px; /* Отступ сверху для свободного места под фиксированным заголовком */\n }\n\n input[type=\"text\"], input[type=\"password\"] {\n width: calc(100% - 20px); /* Учитываем отступы */\n padding: 10px;\n margin: 10px 0;\n border-radius: 5px;\n border: 2px solid #00ff7f;\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin: 10px 0; /* Отступы между кнопками */\n width: 100%; /* Заполняем весь доступный размер бокса */\n box-sizing: border-box; /* Учитываем отступы в ширине */\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .error-message {\n color: red;\n display: none; /* Скрыто по умолчанию */\n }\n\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"box\"\u003e \u003c!-- Добавляем контейнер с классом box --\u003e\n \u003csection\u003e\n \u003ch3\u003eИнформация о разработчике\u003c/h3\u003e\n \u003cdiv id=\"developer-info\"\u003e\n \u003cinput type=\"text\" id=\"email\" placeholder=\"Email\"\u003e\n \u003cinput type=\"text\" id=\"nickname\" placeholder=\"Никнейм\"\u003e\n \u003cinput type=\"password\" id=\"old-password\" placeholder=\"Старый пароль\"\u003e\n \u003cinput type=\"password\" id=\"new-password\" placeholder=\"Новый пароль\"\u003e\n \u003cbutton id=\"update-developer\"\u003eИзменить информацию\u003c/button\u003e\n \u003cp class=\"error-message\" id=\"error-message\"\u003eОшибка при обновлении информации.\u003c/p\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"logout\"\u003eВыйти из админки\u003c/button\u003e \u003c!-- Кнопка выхода --\u003e\n \u003c/section\u003e\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const emailInput = document.getElementById('email');\n const nicknameInput = document.getElementById('nickname');\n const oldPasswordInput = document.getElementById('old-password');\n const newPasswordInput = document.getElementById('new-password');\n const updateDeveloperButton = document.getElementById('update-developer');\n const logoutButton = document.getElementById('logout');\n const errorMessage = document.getElementById('error-message');\n\n const backendUrl = `${window.location.origin}/api/v1/developers`;\n\n let isRetred = false;\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return;\n }\n\n isRetred = true;\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc();\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.developer) {\n emailInput.value = data.result.developer.email;\n nicknameInput.value = data.result.developer.nickname;\n } else {\n if (data.error) {\n handleApiError(data.error, fetchDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const updateDeveloperInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/updateDeveloper`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n developer: {\n email: emailInput.value,\n nickname: nicknameInput.value,\n password: oldPasswordInput.value,\n new_password: newPasswordInput.value,\n }\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result) {\n alert('Информация успешно обновлена');\n } else {\n if (data.error) {\n handleApiError(data.error, updateDeveloperInfo);\n }\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n fetchDeveloperInfo();\n };\n\n const getCookie = (name) =\u003e {\n const parts = document.cookie.split(';');\n for (let i = 0; i \u003c parts.length; i++) {\n const part = parts[i].trim();\n if (part.indexOf(name + '=') === 0) return part.split('=')[1];\n }\n return null;\n };\n\n updateDeveloperButton.addEventListener('click', updateDeveloperInfo);\n logoutButton.addEventListener('click', () =\u003e {\n document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем токен\n document.cookie = 'refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; // Удаляем refresh токен\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n });\n\n fetchDeveloperInfo(); // Запрашиваем информацию о разработчике\n });\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const RuDocs = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eДокументация API\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c); /* Градиент фона */\n color: #ffffff; /* Цвет текста */\n display: flex;\n flex-direction: column;\n min-height: 100vh; /* Минимальная высота на весь экран */\n margin-bottom: 100px;\n margin-top: 100px;\n }\n\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n h1 {\n color: #00ff7f; /* Яркий цвет заголовка */\n text-align: center;\n margin-bottom: 20px;\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Тень для заголовка */\n }\n\n .accordion {\n background: #3a3a3a; /* Цвет фона аккордеона */\n border-radius: 5px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); /* Тень для аккордеона */\n }\n\n .accordion-title {\n padding: 15px;\n cursor: pointer;\n border-bottom: 1px solid #444; /* Углубленный цвет */\n background: #4a4a4a; /* Цвет заголовка аккордеона */\n font-weight: bold;\n transition: background 0.3s, color 0.3s;\n }\n\n .accordion-title:hover {\n background: #5a5a5a; /* Цвет при наведении на заголовок */\n color: #00ff7f; /* Цвет текста при наведении */\n }\n\n .accordion-content {\n display: none;\n padding: 10px;\n background: #2b2b2b; /* Цвет фона содержимого аккордеона */\n }\n\n .active {\n background: #6a6a6a; /* Цвет активного заголовка */\n color: #00ff7f; /* Цвет текста активного заголовка */\n }\n\n pre {\n background: #1e1e1e; /* Цвет фона для блоков кода */\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 3px;\n overflow-x: auto;\n color: #ffffff; /* Цвет текста в блоке кода */\n }\n\n h3, h4, h5 {\n color: #ffffff; /* Цвет заголовков внутри аккордеона */\n }\n\n p {\n color: #cccccc; /* Цвет обычного текста внутри аккордеона */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\u003cbody\u003e\n\u003ch1\u003eДокументация API\u003c/h1\u003e\n{{ range . }} \u003c!-- Обработка каждой категории --\u003e\n\u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .CategoryTitle }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n {{ range .DocStruct }} \u003c!-- Обработка каждого документа в категории --\u003e\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Title }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Info }}\u003c/p\u003e\n {{ if .DockLists }}\n \u003ch5\u003eСписки структур документа:\u003c/h5\u003e\n {{ range .DockLists }}\n \u003ch3\u003e{{ .Title }}\u003c/h3\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003ch5\u003eПуть запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestPath }}\u003c/pre\u003e\n \u003ch5\u003eМетод:\u003c/h5\u003e\n \u003cpre\u003e{{ .Method }}\u003c/pre\u003e\n \u003ch5\u003eЗаголовки запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestHeader }}\u003c/pre\u003e\n \u003ch5\u003eТело запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о теле запроса:\u003c/h5\u003e\n \u003cp\u003e{{ .RequestBodyInfo }}\u003c/p\u003e\n \u003ch5\u003eОтвет:\u003c/h5\u003e\n \u003cpre\u003e{{ .ResponseBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о ответе:\u003c/h5\u003e\n \u003cp\u003e{{ .ResponseBodyInfo }}\u003c/p\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eНет структур документа.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n\n {{ if .DocErrorList }}\n \u003ch5\u003eСписок ошибок:\u003c/h5\u003e\n {{ range .DocErrorList }}\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Name }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003cpre\u003e{{ .Body }}\u003c/pre\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eОшибки не найдены.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n\u003c/div\u003e\n{{ end }}\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\u003cscript\u003e\n const titles = document.querySelectorAll('.accordion-title');\n titles.forEach(title =\u003e {\n title.addEventListener('click', function() {\n this.classList.toggle('active');\n const content = this.nextElementSibling;\n content.style.display = content.style.display === 'block' ? 'none' : 'block';\n });\n });\n\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n `;\n }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const RuDocs = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eДокументация API\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 20px;\n background: linear-gradient(135deg, #2a2a2a, #1c1c1c); /* Градиент фона */\n color: #ffffff; /* Цвет текста */\n display: flex;\n flex-direction: column;\n min-height: 100vh; /* Минимальная высота на весь экран */\n margin-bottom: 100px;\n margin-top: 100px;\n }\n\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n h1 {\n color: #00ff7f; /* Яркий цвет заголовка */\n text-align: center;\n margin-bottom: 20px;\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Тень для заголовка */\n }\n\n .accordion {\n background: #3a3a3a; /* Цвет фона аккордеона */\n border-radius: 5px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); /* Тень для аккордеона */\n }\n\n .accordion-title {\n padding: 15px;\n cursor: pointer;\n border-bottom: 1px solid #444; /* Углубленный цвет */\n background: #4a4a4a; /* Цвет заголовка аккордеона */\n font-weight: bold;\n transition: background 0.3s, color 0.3s;\n }\n\n .accordion-title:hover {\n background: #5a5a5a; /* Цвет при наведении на заголовок */\n color: #00ff7f; /* Цвет текста при наведении */\n }\n\n .accordion-content {\n display: none;\n padding: 10px;\n background: #2b2b2b; /* Цвет фона содержимого аккордеона */\n }\n\n .active {\n background: #6a6a6a; /* Цвет активного заголовка */\n color: #00ff7f; /* Цвет текста активного заголовка */\n }\n\n\n pre {\n background: #1e1e1e; /* Цвет фона для блоков кода */\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 3px;\n overflow-x: auto;\n color: #ffffff; /* Цвет текста в блоке кода */\n }\n\n h3, h4, h5 {\n color: #ffffff; /* Цвет заголовков внутри аккордеона */\n }\n\n p {\n color: #cccccc; /* Цвет обычного текста внутри аккордеона */\n }\n\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\u003cbody\u003e\n\u003ch1\u003eДокументация API\u003c/h1\u003e\n{{ range . }} \u003c!-- Обработка каждой категории --\u003e\n\u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .CategoryTitle }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n {{ range .DocStruct }} \u003c!-- Обработка каждого документа в категории --\u003e\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Title }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Info }}\u003c/p\u003e\n {{ if .DockLists }}\n \u003ch5\u003eСписки структур документа:\u003c/h5\u003e\n {{ range .DockLists }}\n \u003ch3\u003e{{ .Title }}\u003c/h3\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003ch5\u003eПуть запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestPath }}\u003c/pre\u003e\n \u003ch5\u003eМетод:\u003c/h5\u003e\n \u003cpre\u003e{{ .Method }}\u003c/pre\u003e\n \u003ch5\u003eЗаголовки запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestHeader }}\u003c/pre\u003e\n \u003ch5\u003eТело запроса:\u003c/h5\u003e\n \u003cpre\u003e{{ .RequestBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о теле запроса:\u003c/h5\u003e\n \u003cp\u003e{{ .RequestBodyInfo }}\u003c/p\u003e\n \u003ch5\u003eОтвет:\u003c/h5\u003e\n \u003cpre\u003e{{ .ResponseBody }}\u003c/pre\u003e\n \u003ch5\u003eИнформация о ответе:\u003c/h5\u003e\n \u003cp\u003e{{ .ResponseBodyInfo }}\u003c/p\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eНет структур документа.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n\n {{ if .DocErrorList }}\n \u003ch5\u003eСписок ошибок:\u003c/h5\u003e\n {{ range .DocErrorList }}\n \u003cdiv class=\"accordion\"\u003e\n \u003cdiv class=\"accordion-title\"\u003e{{ .Name }}\u003c/div\u003e\n \u003cdiv class=\"accordion-content\"\u003e\n \u003cp\u003e{{ .Description }}\u003c/p\u003e\n \u003cpre\u003e{{ .Body }}\u003c/pre\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n {{ end }}\n {{ else }}\n \u003cp\u003eОшибки не найдены.\u003c/p\u003e\n {{ end }}\n \u003c/div\u003e\n\u003c/div\u003e\n{{ end }}\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\u003cscript\u003e\n const titles = document.querySelectorAll('.accordion-title');\n titles.forEach(title =\u003e {\n title.addEventListener('click', function() {\n this.classList.toggle('active');\n const content = this.nextElementSibling;\n content.style.display = content.style.display === 'block' ? 'none' : 'block';\n });\n });\n\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n // Проверяем наличие токенов в куках\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n // Получаем элемент навигации\n const navLinks = document.getElementById('nav-links');\n\n // В зависимости от наличия токенов, показываем соответствующие кнопки\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n `;\n }\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const RuGameCollection = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКоллекция Игр\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 5% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #games-collection {\n padding: 80px 20px; /* Отступ для секции */\n text-align: center;\n }\n\n #games-box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px; /* Пробел между играми */\n }\n\n .game-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n cursor: pointer; /* Курсор в виде указателя */\n }\n\n .game-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .button-container {\n margin-top: 20px; /* Отступ сверху для кнопок */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"games-collection\"\u003e\n \u003ch3\u003eКоллекция Игр\u003c/h3\u003e\n \u003cdiv id=\"games-box\"\u003e\n \u003c!-- Здесь будет отображаться список игр --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-game\"\u003eДобавить игру\u003c/button\u003e\n \u003cbutton id=\"refresh-list\"\u003eОбновить список\u003c/button\u003e\n \u003cbutton id=\"delete-game\"\u003eУдалить игру\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eИгры не найдены.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gamesBox = document.getElementById('games-box');\n const errorMessage = document.getElementById('error-message');\n const addGameButton = document.getElementById('add-game');\n const refreshListButton = document.getElementById('refresh-list');\n const deleteGameButton = document.getElementById('delete-game');\n const developerInfoButton = document.getElementById('developer-info');\n\n // Определяем базовый URL для бэкенда\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGames = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getMyGames`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.games.length \u003e 0) {\n errorMessage.style.display = 'none'; // Скрываем сообщение об ошибке\n renderGames(data.result.games);\n } else {\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = ''; // Очищаем контейнер с играми\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const deleteGameByID = async () =\u003e {\n const deletingID = prompt('Введите gameID\\n\\n' +\n 'Помните что после удаления игры все пользователи автоматически удаляться\\n' +\n 'Все информация об игре будет удаленна безвозвратно');\n\n if (!deletingID) {\n return\n }\n\n try {\n const response = await fetch(`${backendUrl}/deleteGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID: deletingID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = '';\n errorMessage.style.display = 'block';\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block';\n }\n fetchGames();\n };\n\n const renderGames = (games) =\u003e {\n gamesBox.innerHTML = ''; // Очищаем контейнер перед добавлением новых игр\n games.forEach(game =\u003e {\n const gameItem = document.createElement('div');\n gameItem.className = 'game-item';\n gameItem.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n \u003cp\u003eКоличество игроков: ${game.count_players}\u003c/p\u003e\n \u003cp\u003eКоличество игроков онлайн: ${game.count_online}\u003c/p\u003e\n `;\n gameItem.onclick = () =\u003e {\n window.location.href = `game_info?gameID=${game.gameID}`; // Редирект на страницу с игрой\n };\n gamesBox.appendChild(gameItem);\n });\n };\n\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n addGameButton.onclick = () =\u003e {\n window.location.href = 'game_info'; // Редирект на страницу добавления игры\n };\n\n refreshListButton.onclick = fetchGames;\n deleteGameButton.onclick = deleteGameByID;\n\n fetchGames();\n\n developerInfoButton.onclick = () =\u003e {\n window.location.href = 'developer-info';\n };\n\n // Первоначальный вызов для загрузки игр\n\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const RuGameCollection = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКоллекция Игр\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 5% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #games-collection {\n padding: 80px 20px; /* Отступ для секции */\n text-align: center;\n }\n\n #games-box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px; /* Пробел между играми */\n }\n\n .game-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n cursor: pointer; /* Курсор в виде указателя */\n }\n\n .game-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .button-container {\n margin-top: 20px; /* Отступ сверху для кнопок */\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e \u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"games-collection\"\u003e\n \u003ch3\u003eКоллекция Игр\u003c/h3\u003e\n \u003cdiv id=\"games-box\"\u003e\n \u003c!-- Здесь будет отображаться список игр --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-game\"\u003eДобавить игру\u003c/button\u003e\n \u003cbutton id=\"refresh-list\"\u003eОбновить список\u003c/button\u003e\n \u003cbutton id=\"delete-game\"\u003eУдалить игру\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eИгры не найдены.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gamesBox = document.getElementById('games-box');\n const errorMessage = document.getElementById('error-message');\n const addGameButton = document.getElementById('add-game');\n const refreshListButton = document.getElementById('refresh-list');\n const deleteGameButton = document.getElementById('delete-game');\n const developerInfoButton = document.getElementById('developer-info');\n\n // Определяем базовый URL для бэкенда\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGames = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getMyGames`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.games.length \u003e 0) {\n errorMessage.style.display = 'none'; // Скрываем сообщение об ошибке\n renderGames(data.result.games);\n } else {\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = ''; // Очищаем контейнер с играми\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block'; // Показываем сообщение об ошибке\n }\n };\n\n const deleteGameByID = async () =\u003e {\n const deletingID = prompt('Введите gameID\\n\\n' +\n 'Помните что после удаления игры все пользователи автоматически удаляться\\n' +\n 'Все информация об игре будет удаленна безвозвратно');\n\n if (!deletingID) {\n return\n }\n\n try {\n const response = await fetch(`${backendUrl}/deleteGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token') // Получаем токен из куки\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID: deletingID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error,fetchGames)\n }\n gamesBox.innerHTML = '';\n errorMessage.style.display = 'block';\n } catch (error) {\n console.error('Ошибка:', error);\n errorMessage.style.display = 'block';\n }\n fetchGames();\n };\n\n const renderGames = (games) =\u003e {\n gamesBox.innerHTML = ''; // Очищаем контейнер перед добавлением новых игр\n games.forEach(game =\u003e {\n const gameItem = document.createElement('div');\n gameItem.className = 'game-item';\n gameItem.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n \u003cp\u003eКоличество игроков: ${game.count_players}\u003c/p\u003e\n \u003cp\u003eКоличество игроков онлайн: ${game.count_online}\u003c/p\u003e\n `;\n gameItem.onclick = () =\u003e {\n window.location.href = `game_info?gameID=${game.gameID}`; // Редирект на страницу с игрой\n };\n gamesBox.appendChild(gameItem);\n });\n };\n\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n addGameButton.onclick = () =\u003e {\n window.location.href = 'game_info'; // Редирект на страницу добавления игры\n };\n\n refreshListButton.onclick = fetchGames;\n deleteGameButton.onclick = deleteGameByID;\n\n fetchGames();\n\n developerInfoButton.onclick = () =\u003e {\n window.location.href = 'developer-info';\n };\n\n // Первоначальный вызов для загрузки игр\n\n });\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" const RuGameConfig = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003ctitle\u003eКонфигурация Игры\u003c/title\u003e\n \u003cstyle\u003e\n /* Основные стили */\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n #config-section {\n padding: 80px 20px;\n display: flex;\n }\n\n #config-list {\n width: 30%;\n padding-right: 20px;\n }\n\n\n #config-description {\n width: 70%;\n border-left: 1px solid #00ff7f;\n padding-left: 20px;\n }\n\n .config-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n padding: 15px;\n margin-bottom: 10px;\n cursor: pointer;\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .config-item:hover {\n transform: scale(1.05);\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5);\n }\n\n .button-container {\n margin-top: 20px;\n }\n\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n pre {\n background: rgba(255, 255, 255, 0.1);\n padding: 10px;\n border-radius: 5px;\n overflow-x: auto;\n }\n\n .config-form {\n display: none;\n margin-top: 20px;\n background: rgba(255, 255, 255, 0.1);\n padding: 15px;\n border-radius: 10px;\n }\n\n .form-group {\n margin-bottom: 15px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 5px;\n }\n\n .form-group input,\n .form-group select {\n width: calc(20%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n font-size: 12px;\n }\n\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: 1px solid #00ff7f;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003csection id=\"config-section\"\u003e\n \u003cdiv id=\"config-list\"\u003e\n \u003ch3\u003eКонфигурации\u003c/h3\u003e\n \u003cdiv id=\"configs-box\"\u003e\n \u003c!-- Список конфигураций --\u003e\n \u003c/div\u003e\n \u003cdiv class=\"button-container\"\u003e\n \u003cbutton id=\"add-config\"\u003eДобавить конфигурацию\u003c/button\u003e\n \u003c/div\u003e\n \u003cp id=\"error-message\" style=\"display:none;\"\u003eКонфигурации не найдены.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv id=\"config-description\"\u003e\n \u003ch3\u003eОписание Конфигурации\u003c/h3\u003e\n \u003cpre id=\"config-json\"\u003e\n \u003cbutton id=\"refresh-result\" style=\"margin: 10px; float: right;\"\u003eОбновить результат\u003c/button\u003e\n Выберите конфигурацию, чтобы увидеть её описание.\n \u003c/pre\u003e\n \u003cdiv class=\"config-form\" id=\"config-form\"\u003e\n \u003ch3\u003eДобавление Конфигурации\u003c/h3\u003e\n\n \u003c!-- Выпадающий список действий --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"run_func\"\u003eВыберите действие:\u003c/label\u003e\n \u003cselect id=\"run_func\"\u003e\n \u003coption value=\"IncrementResult\"\u003eДобовляем к полю +1 {IncrementResult}\u003c/option\u003e\n \u003coption value=\"DecrementResult\"\u003eУдаляем из поля -1 {DecrementResult}\u003c/option\u003e\n \u003coption value=\"AdditionDataResultToOld\"\u003eСуммируем результаты oldData + newData {AdditionDataResultToOld}\u003c/option\u003e\n \u003coption value=\"SubtractDataResultToOld\"\u003eВычитаем результаты oldData - newData {SubtractDataResultToOld}\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_name\"\u003eИмя результата:\u003c/label\u003e\n \u003cinput type=\"text\" id=\"result_name\" placeholder=\"Введите имя результата\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"result_type\"\u003eТип результата:\u003c/label\u003e\n \u003cselect id=\"result_type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003c!-- Параметры пары значений --\u003e\n \u003cdiv id=\"pairs-container\"\u003e\n \u003cdiv class=\"pair-fields\"\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n \u003cbutton id=\"add-pair\"\u003eДобавить пару\u003c/button\u003e\n\n \u003c!-- Выбор типа сервера --\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"use_on_server_type\"\u003eТип сервера:\u003c/label\u003e\n \u003cselect id=\"use_on_server_type\"\u003e\n \u003coption value=\"UDP\"\u003eUDP\u003c/option\u003e\n \u003coption value=\"TCP\"\u003eTCP\u003c/option\u003e\n \u003coption value=\"WS\"\u003eWebSocket\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n\n \u003cbutton id=\"save-config\"\u003eСохранить\u003c/button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const configsBox = document.getElementById('configs-box');\n const errorMessage = document.getElementById('error-message');\n const addConfigButton = document.getElementById('add-config');\n const configJson = document.getElementById('config-json');\n const configForm = document.getElementById('config-form');\n const saveConfigButton = document.getElementById('save-config');\n const addPairButton = document.getElementById('add-pair');\n const pairsContainer = document.getElementById('pairs-container');\n const refreshResultButton = document.getElementById('refresh-result'); // Кнопка обновления результата\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID'); // Получаем gameID из GET-параметра\n\n const backendUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameConfig`;\n const gameResultConfigUrl = `${window.location.origin}/api/v1/devToolsGameConfigs/getGameResultConfigPreview`;\n let existingConfigs = []; // Массив для хранения текущих конфигураций\n\n // Добавляем токен авторизации\n let token = getCookie(\"token\");\n\n let isRetred = false\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n }else{\n alert(error.message);\n }\n };\n\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n token = getCookie(\"token\")\n };\n\n // Получение текущих конфигураций\n function fetchGameConfig() {\n fetch(backendUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameConfig)\n }\n renderGameConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching configurations:', err);\n });\n }\n\n // Получение результата конфигурации игры\n function fetchGameResultConfigPreview() {\n fetch(gameResultConfigUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n gameID: gameID\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, fetchGameResultConfigPreview)\n }\n renderGameResultConfig(data)\n })\n .catch(err =\u003e {\n console.error('Error fetching game result config:', err);\n });\n }\n\n // Отображение конфигураций\n function renderGameConfig(data) {\n if (!data.result || !data.result.configs || data.result.configs.sorting_config.length === 0) {\n errorMessage.style.display = 'block';\n } else {\n errorMessage.style.display = 'none';\n existingConfigs = data.result.configs.sorting_config; // Сохраняем текущие конфигурации\n configsBox.innerHTML = ''; // Очищаем перед рендерингом\n\n data.result.configs.sorting_config.forEach((config, index) =\u003e {\n const configItem = document.createElement('div');\n configItem.className = 'config-item';\n configItem.style.display = 'flex';\n configItem.style.alignItems = 'center'; // Выравнивание по центру\n\n // Кнопка для удаления конфигурации\n const deleteButton = document.createElement('button');\n deleteButton.textContent = 'X';\n deleteButton.style.marginRight = '10px'; // Отступ справа от кнопки\n deleteButton.style.backgroundColor = 'gray';\n deleteButton.style.color = '#fff';\n deleteButton.style.border = 'none';\n deleteButton.style.cursor = 'pointer';\n deleteButton.addEventListener('click', (e) =\u003e {\n e.stopPropagation(); // Чтобы не вызывалось событие клика на configItem\n deleteConfig(index);\n });\n\n configItem.appendChild(deleteButton); // Добавляем кнопку \"Удалить\" в элемент конфигурации\n configItem.appendChild(document.createTextNode(`${config.result_name}: ${config.name}`)); // Текст конфигурации\n\n configItem.addEventListener('click', () =\u003e {\n configJson.textContent = JSON.stringify(config, null, 2);\n configJson.appendChild(refreshResultButton);\n });\n configsBox.appendChild(configItem);\n });\n }\n }\n\n // Удаление конфигурации\n function deleteConfig(index) {\n existingConfigs.splice(index, 1); // Удаляем элемент по индексу\n update(); // Обновляем конфигурации на сервере\n }\n\n // Отображение результата конфигурации игры\n function renderGameResultConfig(data) {\n if (!data.result || !data.result.gameResult) {\n configJson.textContent = 'Нет данных для отображения.';\n configJson.appendChild(refreshResultButton);\n return;\n }\n\n const formattedJson = JSON.stringify(data.result.gameResult, null, 2);\n configJson.textContent = formattedJson;\n configJson.appendChild(refreshResultButton);\n }\n\n // Функция для обновления конфигураций на сервере\n function update() {\n const updatedConfigs = {\n gameID: gameID,\n sorting_config: existingConfigs\n };\n\n fetch(`${window.location.origin}/api/v1/devToolsGameConfigs/createOrUpdateConfig`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': `${token}`\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: '2.0',\n params: {\n configs: updatedConfigs\n }\n })\n })\n .then(response =\u003e response.json())\n .then(data =\u003e {\n if (data.error) {\n handleApiError(data.error, update)\n }\n })\n .catch(err =\u003e {\n\n console.error('Ошибка при отправке запроса:', err);\n })\n .finally(() =\u003e {\n fetchGameConfig(); // Обновляем конфигурации после обновления на сервере\n });\n }\n\n // Инициализация\n fetchGameConfig();\n fetchGameResultConfigPreview();\n\n // Обработчик для кнопки обновления результата\n refreshResultButton.addEventListener('click', fetchGameResultConfigPreview);\n\n addConfigButton.addEventListener('click', () =\u003e {\n configForm.style.display = 'block';\n });\n\n saveConfigButton.addEventListener('click', () =\u003e {\n const use_on_server_type = document.getElementById('use_on_server_type').value;\n const name = document.getElementById('run_func').value;\n const result_name = document.getElementById('result_name').value;\n const result_type = document.getElementById('result_type').value;\n\n // Собираем значения для параметров пар\n const pairs = Array.from(document.getElementsByClassName('pair-fields')).map(pairField =\u003e {\n const column_name = pairField.querySelector('.column-name').value;\n const value_type = pairField.querySelector('.value-type').value;\n return { column_name, value_type };\n });\n\n // Новая конфигурация\n const newConfig = {\n use_on_server_type: use_on_server_type,\n name: name,\n result_name: result_name,\n result_type: result_type,\n params: pairs\n };\n\n // Добавляем новую конфигурацию в массив existingConfigs\n existingConfigs.push(newConfig);\n update(); // Обновляем конфигурации на сервере после добавления\n });\n\n addPairButton.addEventListener('click', () =\u003e {\n const newPair = document.createElement('div');\n newPair.className = 'pair-fields';\n newPair.innerHTML = `\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-column-name\"\u003eИмя столбца:\u003c/label\u003e\n \u003cinput type=\"text\" class=\"column-name\" placeholder=\"Введите имя столбца\"\u003e\n \u003c/div\u003e\n \u003cdiv class=\"form-group\"\u003e\n \u003clabel for=\"param-value-type\"\u003eТип значения:\u003c/label\u003e\n \u003cselect class=\"value-type\"\u003e\n \u003coption value=\"int\"\u003eInteger\u003c/option\u003e\n \u003coption value=\"float\"\u003eFloat\u003c/option\u003e\n \u003coption value=\"string\"\u003eString\u003c/option\u003e\n \u003c/select\u003e\n \u003c/div\u003e\n `;\n pairsContainer.appendChild(newPair);\n });\n\n function getCookie(name) {\n const matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n });\n\u003c/script\u003e\n\n\n\u003c/body\u003e\n\u003c/html\u003e\n" -const RuGameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e--\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const RuGameInfo = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\"\u003e\u003c/script\u003e\n \u003ctitle\u003eИнформация об Игре\u003c/title\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n margin-bottom: 100px;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive; /* Курсивный шрифт для логотипа */\n color: #00ff7f;\n margin-left: 5%; /* Сдвиг логотипа на 10% влево */\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%;\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .container {\n display: flex;\n height: 100vh;\n padding-top: 80px;\n }\n\n #game-info,\n #servers-box {\n flex: 1;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n position: relative;\n overflow-y: auto;\n border: 1px solid #ccc;\n margin: 10px; /* Упрощение отступов */\n box-sizing: border-box;\n }\n\n h4 {\n margin: 20px; /* Отступы для заголовков */\n }\n\n pre {\n background: #2e2e2e;\n padding: 10px;\n border-radius: 5px;\n overflow: auto;\n margin: 20px;\n }\n\n\n code {\n color: #00ff7f;\n }\n\n .game-config,\n .add-server-button,\n button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 15px;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-top: 10px; /* Отступ сверху для кнопок */\n margin-left: 10px; /* Отступ сверху для кнопок */\n margin-right: 10px; /* Отступ сверху для кнопок */\n }\n\n .game-config:hover,\n .add-server-button:hover,\n button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n }\n\n .server-item {\n background: rgba(255, 255, 255, 0.1);\n border-radius: 10px;\n margin: 10px; /* Упрощение отступов */\n border: 1px solid #ddd;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n }\n\n .server-actions {\n display: flex;\n gap: 10px; /* Пробел между кнопками действий */\n }\n\n footer {\n position: fixed; /* Фиксируем футер внизу */\n bottom: 0;\n left: 0;\n width: 100%;\n text-align: center;\n padding: 10px;\n background-color: rgba(0, 0, 0, 0.8);\n }\n\n /* Дополнительные стили для ввода */\n input[type=\"text\"] {\n width: calc(80%);\n padding: 10px;\n border: none;\n border-radius: 5px;\n background: rgba(255, 255, 255, 0.2);\n color: #fff;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n input[type=\"text\"]::placeholder {\n color: #ccc; /* Цвет плейсхолдера */\n }\n\n /* Стили для кнопки \"Сохранить изменения\" */\n .create-game,\n .save-game {\n background: #00ff7f;\n color: #1c1c1c;\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .create-game:hover,\n .save-game:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n .player-count,\n .online-player-count,\n .description-label {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .eng-button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 10px 20px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s;\n }\n\n . eng-button:hover {\n background: rgba(0, 255, 127, 0.8);\n transform: scale(1.05);\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"/admin/info\"\u003eИнформация о разработчике\u003c/a\u003e\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\u003cdiv class=\"container\"\u003e\n \u003csection id=\"game-info\"\u003e\n \u003ch4\u003eИнформация об Игре\u003c/h4\u003e\n \u003cdiv id=\"game-details\"\u003e\u003c/div\u003e\n \u003cdiv id=\"new-game-container\" style=\"display:none;\"\u003e\n \u003ch4\u003eСоздать новую игру\u003c/h4\u003e\n \u003cinput type=\"text\" id=\"new-game-name\" placeholder=\"Введите имя игры\"/\u003e\n \u003cbutton class=\"create-game\" id=\"create-game\"\u003eСоздать игру\u003c/button\u003e\n \u003c/div\u003e\n \u003c/section\u003e\n\n \u003cdiv id=\"servers-box\"\u003e\n \u003ch4\u003eСерверы\u003c/h4\u003e\n \u003cbutton class=\"add-server-button\" id=\"add-server-button\"\u003eДобавить сервер\u003c/button\u003e\n \u003cdiv id=\"servers-list\"\u003e\u003c/div\u003e\n \u003c/div\u003e\n\n\u003c/div\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n document.addEventListener('DOMContentLoaded', () =\u003e {\n const gameDetails = document.getElementById('game-details');\n const newGameContainer = document.getElementById('new-game-container');\n const serversList = document.getElementById('servers-list');\n\n const urlParams = new URLSearchParams(window.location.search);\n const gameID = urlParams.get('gameID');\n\n let isRetred = false\n\n\n const backendUrl = `${window.location.origin}/api/v1/devTools`;\n\n let gameServers = []; // Хранит массив ID серверов игры\n\n let deletingServerID = \"\"\n let newServerName = \"\"\n let newServerAddress = \"\"\n\n const handleApiError = async (error, retryFunc) =\u003e {\n console.error('Ошибка:', error);\n if (error.message \u0026\u0026 error.message.includes('token')) {\n await refreshToken(retryFunc);\n } else {\n alert(error.message);\n }\n };\n\n\n const refreshToken = async (retryFunc) =\u003e {\n const refreshToken = getCookie('refresh');\n const token = getCookie('token');\n if (isRetred) {\n return\n }\n\n isRetred = true\n\n try {\n const response = await fetch(`${window.location.origin}/api/v1/developers/refreshToken`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': token,\n 'RefreshToken': refreshToken\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n if (!response.ok) throw new Error('Ошибка обновления токенов');\n\n const data = await response.json();\n if (data.result \u0026\u0026 data.result.newToken \u0026\u0026 data.result.newRefresh) {\n document.cookie = `token=${data.result.newToken}; path=/`;\n document.cookie = `refresh=${data.result.newRefresh}; path=/`;\n retryFunc()\n } else {\n throw new Error('Ошибка обновления токенов');\n }\n } catch (error) {\n console.error('Ошибка обновления токенов:', error);\n window.location.href = '/admin/auth'; // Редирект на страницу аутентификации\n }\n };\n\n const fetchGameInfo = async () =\u003e {\n try {\n const response = await fetch(`${backendUrl}/getGameByGameID`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {gameID}\n })\n });\n\n const data = await response.json();\n\n if (data.result \u0026\u0026 data.result.game) {\n renderGameInfo(data.result.game);\n gameServers = data.result.game.servers; // Сохраняем массив серверов игры\n fetchServers(gameID);\n }\n if (data.error) {\n handleApiError(data.error, fetchGameInfo);\n }\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n\n const renderGameInfo = (game) =\u003e {\n gameDetails.innerHTML = `\n \u003ch4\u003e${game.name}\u003c/h4\u003e\n\n \u003cdiv class=\"player-count\"\u003eКоличество игроков: ${game.count_players}\u003c/div\u003e\n \u003cdiv class=\"online-player-count\"\u003eКоличество игроков онлайн: ${game.count_online}\u003c/div\u003e\n\u003c!-- \u003cdiv class=\"description-label\"\u003eОписание: ${game.description}\u003c/div\u003e--\u003e\n \u003cinput type=\"text\" class=\"edit-game-name\" id=\"edit-game-name\" value=\"${game.name}\" /\u003e\n \u003cpre\u003e\u003ccode class=\"gameID show\"\u003egameID: \"${game.gameID}\"\u003c/code\u003e\u003c/pre\u003e\n\n \u003cbutton class=\"save-game\" id=\"save-game\"\u003eСохранить изменения\u003c/button\u003e\n\u003c!-- \u003cbutton class=\"game-config\" id=\"game-config\"\u003eУдаленные процедуры\u003c/button\u003e--\u003e\n\u003c!-- \u003cbutton class=\"game-annalists\" id=\"game-annalists\"\u003eАналитика\u003c/button\u003e--\u003e\n\n `;\n document.getElementById('save-game').onclick = () =\u003e fetchUpdateGame();\n document.getElementById('game-config').onclick = () =\u003e fetchGameConfigs();\n // document.getElementById('game-annalists').onclick = () =\u003e fetchGameAnnalists();\n };\n\n const fetchUpdateGame = async () =\u003e {\n const newName = document.getElementById('edit-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/updateGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n newGame: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchUpdateGame);\n }\n\n alert('Игра обновлена успешно!');\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchCreateGame = async () =\u003e {\n const newName = document.getElementById('new-game-name').value;\n try {\n const response = await fetch(`${backendUrl}/createGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameID,\n game: {name: newName}\n }\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateGame);\n } else {\n const id = data.result.id\n window.location.href = `game_info?gameID=${id}`\n alert('Игра обновлена успешно!');\n }\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const fetchServers = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/getServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {}\n })\n });\n\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchServers);\n }\n\n renderServers(data.result.servers);\n } catch (error) {\n console.error('Ошибка:', error);\n }\n };\n\n const renderServers = (servers) =\u003e {\n serversList.innerHTML = '';\n servers.forEach(server =\u003e {\n const serverItem = document.createElement('div');\n serverItem.className = 'server-item';\n serverItem.innerHTML = `\n \u003cp\u003eИмя: ${server.server_name}\u003c/p\u003e\n \u003cp\u003eАдрес: ${server.address}\u003c/p\u003e\n \u003cp\u003eТип: ${server.server_type}\u003c/p\u003e\n \u003cp\u003eРегион: ${server.region}\u003c/p\u003e\n \u003cdiv class=\"server-actions\"\u003e\n ${!server.ascenmmo_server ?\n `\u003cbutton class=\"delete-button\" data-server-id=\"${server.id}\"\u003eУдалить сервер\u003c/button\u003e` : ''}\n ${gameServers.includes(server.id) ?\n `\u003cbutton class=\"disconnect-button\" data-server-id=\"${server.id}\"\u003eОтключить сервер\u003c/button\u003e` :\n `\u003cbutton class=\"connect-button\" data-server-id=\"${server.id}\"\u003eПодключить сервер\u003c/button\u003e`}\n \u003c/div\u003e\n `;\n serversList.appendChild(serverItem);\n });\n };\n\n // Привязываем функции к глобальному объекту\n window.connectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOnServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} подключен!`, data);\n // Изменяем текст кнопки на \"Отключить сервер\"\n button.classList.remove('connect-button');\n button.classList.add('disconnect-button');\n button.textContent = 'Отключить сервер';\n } catch (error) {\n console.error('Ошибка подключения:', error);\n }\n };\n\n window.disconnectServer = async (serverID, button) =\u003e {\n try {\n const response = await fetch(`${backendUrl}/turnOffServerInGame`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n gameId: gameID,\n serverID: serverID\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n\n const data = await response.json();\n console.log(`Сервер ${serverID} отключен!`, data);\n // Изменяем текст кнопки на \"Подключить сервер\"\n button.classList.remove('disconnect-button');\n button.classList.add('connect-button');\n button.textContent = 'Подключить сервер';\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n\n const fetchCreateServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/addServer`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {\n address: newServerAddress,\n name: newServerName\n }\n })\n });\n\n if (!response.ok) throw new Error('Ошибка сети');\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchCreateServer)\n } else {\n fetchGameInfo()\n }\n } catch (error) {\n console.error('Ошибка отключения:', error);\n }\n };\n\n const fetchDeleteServer = async () =\u003e {\n try {\n const response = await fetch(`${window.location.origin}/api/v1/devToolsServer/deleteServers`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Token': getCookie('token')\n },\n body: JSON.stringify({\n id: 1,\n jsonrpc: \"2.0\",\n params: {serverID: deletingServerID}\n })\n });\n\n const data = await response.json();\n if (data.error) {\n handleApiError(data.error, fetchDeleteServer)\n } else {\n fetchGameInfo()\n }\n\n } catch (error) {\n console.error('Ошибка:', error);\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания игры\n }\n };\n const getCookie = (name) =\u003e {\n const value = `; ${document.cookie}`;\n const parts = value.split(`; ${name}=`);\n if (parts.length === 2) return parts.pop().split(';').shift();\n };\n\n // Добавляем обработчик события для кнопки \"Добавить сервер\"\n document.getElementById('add-server-button').onclick = () =\u003e {\n const serverName = prompt('Введите имя сервера.\\n' +\n 'Имя будет отображаться в вашей админке:');\n\n if (!serverName) {\n return\n }\n\n const serverAddress = prompt('Введите адрес или доменное имя сервера.\\n' +\n 'Если вы используете не дефолтные порты подалуста введите по примеру:\\n' +\n 'ascenmmo.com:8081 или 127.0.0.1:8081');\n\n if (serverName \u0026\u0026 serverAddress) {\n newServerAddress = serverAddress\n newServerName = serverName\n fetchCreateServer();\n fetchGameInfo();\n }\n };\n\n const deleteServer = (serverID) =\u003e {\n deletingServerID = serverID;\n fetchDeleteServer();\n }\n\n document.getElementById('create-game').onclick = () =\u003e {\n fetchCreateGame();\n };\n\n\n const fetchGameConfigs = async () =\u003e {\n window.location.href = `game_info/config?gameID=${gameID}`\n }\n\n const fetchGameAnnalists = async () =\u003e {\n window.location.href = `game_info/annalists?gameID=${gameID}`\n }\n\n // Добавляем делегирование событий для кнопок\n serversList.addEventListener('click', (event) =\u003e {\n if (event.target.tagName === 'BUTTON') {\n const button = event.target;\n const serverID = button.getAttribute('data-server-id');\n\n if (button.classList.contains('connect-button')) {\n connectServer(serverID, button);\n } else if (button.classList.contains('disconnect-button')) {\n disconnectServer(serverID, button);\n } else if (button.classList.contains('delete-button')) {\n deleteServer(serverID);\n }\n }\n });\n\n if (!gameID) {\n newGameContainer.style.display = 'block'; // Показываем контейнер для создания новой игры\n } else {\n fetchGameInfo();\n }\n });\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" -const RuMainPage = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003cbutton id=\"eng-button\" class=\"button\"\u003eEng\u003c/button\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" +const RuMainPage = "\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"ru\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n \u003ctitle\u003eAscenmmo\u003c/title\u003e\n \u003clink href=\"https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700\u0026display=swap\" rel=\"stylesheet\"\u003e\n \u003cstyle\u003e\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #1c1c1c;\n color: #fff;\n line-height: 1.6;\n }\n\n header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n padding: 20px;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background 0.3s ease;\n z-index: 1000;\n }\n\n header h1 {\n margin: 0;\n font-size: 24px;\n font-family: 'Dancing Script', cursive;\n color: #00ff7f;\n margin-left: 5%;\n text-decoration: none;\n }\n\n header nav {\n display: flex;\n gap: 20px;\n margin-right: 5%; /* Сдвиг кнопок на 10% вправо */\n }\n\n header nav a {\n color: #fff;\n text-decoration: none;\n padding: 10px 20px;\n border: 2px solid #00ff7f;\n border-radius: 5px;\n transition: background 0.3s, color 0.3s;\n }\n\n header nav a:hover {\n background: #00ff7f;\n color: #1c1c1c;\n }\n\n .hero {\n height: 100vh; /* Высота экрана */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n width: 100%; /* Ширина на всю страницу */\n position: relative; /* Для абсолютного позиционирования дочерних элементов */\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .future-container {\n position: absolute; /* Задать абсолютное позиционирование */\n top: 0; /* Сдвиг вверх */\n left: 0; /* Сдвиг влево */\n background: #000; /* Черный фон */\n width: 100%; /* Ширина на всю страницу */\n height: 100%; /* Высота на всю страницу */\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n }\n\n .future-container h2 {\n font-size: 48px;\n margin-bottom: 20px;\n }\n\n .future-container p {\n font-size: 24px;\n margin-bottom: 30px;\n }\n\n .button {\n background: #00ff7f;\n color: #1c1c1c;\n border: none;\n padding: 15px 30px;\n border-radius: 5px;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s, transform 0.3s, box-shadow 0.3s;\n width: 100%; /* Ширина кнопки на весь экран */\n max-width: 300px; /* Максимальная ширина кнопки */\n text-decoration: none;\n }\n\n .button:hover {\n background: rgba(0, 255, 127, 0.8); /* Легкое затемнение */\n transform: scale(1.05);\n box-shadow: 0 0 20px rgba(0, 255, 127, 0.7);\n }\n\n section {\n padding: 60px 20px;\n text-align: center;\n animation: fadeIn 1s ease-in;\n }\n\n h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n }\n\n p {\n margin-bottom: 40px;\n font-size: 18px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n }\n\n footer {\n padding: 10px;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.8);\n position: relative;\n }\n\n\n .service-container {\n display: flex;\n justify-content: center;\n gap: 20px; /* Пространство между элементами */\n flex-wrap: wrap; /* Позволяет элементам оборачиваться на мобильных устройствах */\n padding: 40px 20px; /* Отступы сверху и снизу */\n }\n\n .service-item {\n background: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон для контраста */\n border-radius: 10px;\n padding: 20px;\n max-width: 300px; /* Максимальная ширина для каждого элемента */\n transition: transform 0.3s, box-shadow 0.3s;\n }\n\n .service-item:hover {\n transform: scale(1.05); /* Увеличение при наведении */\n box-shadow: 0 0 15px rgba(0, 255, 127, 0.5); /* Тень при наведении */\n }\n\n .service-item h4 {\n color: #00ff7f; /* Цвет заголовка */\n margin-bottom: 10px; /* Отступ снизу */\n }\n\n .service-item p {\n font-size: 16px; /* Размер текста */\n color: #e0e0e0; /* Цвет текста для лучшего контраста */\n }\n\n #admin {\n padding: 60px 20px;\n text-align: center;\n background: linear-gradient(135deg, #000000 30%, #3d3d3d 100%); /* Градиентный фон */\n border-radius: 10px; /* Закругление углов */\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); /* Тень для глубины */\n position: relative; /* Для позиционирования псевдоэлементов */\n overflow: hidden; /* Скрытие переполненных элементов */\n animation: fadeIn 1s ease-in; /* Появление */\n }\n\n #admin h3 {\n margin-bottom: 20px;\n font-size: 36px;\n color: #00ff7f;\n animation: slideIn 0.5s ease;\n }\n\n #admin p {\n margin-bottom: 40px;\n font-size: 20px;\n color: #e0e0e0;\n }\n\n .download-section {\n background: url('https://example.com/your-background-image.jpg') no-repeat center center/cover; /* Замените на ваше изображение */\n padding: 60px 20px;\n text-align: center;\n color: #fff;\n animation: fadeIn 1s ease-in;\n position: relative;\n overflow: hidden;\n }\n\n .download-section::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 1;\n }\n\n .download-content {\n position: relative;\n z-index: 2;\n }\n\n .download-content h3 {\n font-size: 36px;\n margin-bottom: 20px;\n animation: slideIn 0.5s ease-in-out;\n }\n\n .download-content p {\n font-size: 18px;\n margin-bottom: 10px;\n animation: fadeIn 1s ease-in-out;\n }\n\n .info-text {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 30px;\n color: #ffd700;\n }\n\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cheader\u003e\n \u003ca href=\"/\"\u003e\u003ch1\u003eASCENMMO\u003c/h1\u003e\u003c/a\u003e\n \u003cnav id=\"nav-links\"\u003e\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n\n\n\u003csection class=\"hero\"\u003e\n \u003cdiv class=\"future-container\"\u003e\n \u003ch2\u003eОткройте Будущее Кросс-Платформенных Игр\u003c/h2\u003e\n \u003cp\u003eБесплатные и Мощные Решения для Разработчиков Игр\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('/admin/games');\"\u003eНачать Прямо Сейчас!\u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\n\u003csection id=\"about\"\u003e\n \u003ch3\u003eЧто мы предлагаем?\u003c/h3\u003e\n \u003cdiv class=\"service-container\"\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/udp-server', '_blank');\"\u003e\n \u003ch4\u003e🚀 UDP Service\u003c/h4\u003e\n \u003cp\u003eИспытайте молниеносную передачу данных, которая повысит производительность вашей игры. Идеально подходит\n для динамичных экшенов, наш UDP-сервис гарантирует, что ваши игроки останутся на связи и вовлечены без\n задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/websocket-server', '_blank');\"\u003e\n \u003ch4\u003e🌐 WebSocket Service\u003c/h4\u003e\n \u003cp\u003eОживите вашу игру с помощью реального времени. Наш WebSocket-сервис позволяет мгновенно обновлять и\n взаимодействовать, позволяя игрокам общаться, обмениваться и объединяться без задержек.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv class=\"service-item\" onclick=\"window.open('https://github.com/ascenmmo/tcp-server', '_blank');\"\u003e\n \u003ch4\u003e🔒 TCP Service\u003c/h4\u003e\n \u003cp\u003eНаслаждайтесь надежным и прочным соединением, которое гарантирует целостность данных. Идеально подходит\n для критически важных функций игры, наш TCP-сервис обеспечивает безопасность данных ваших игроков.\u003c/p\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection id=\"admin\"\u003e\n \u003ch3\u003e🎮 Ваша игра, ваш контроль\u003c/h3\u003e\n \u003cp\u003eПрисоединяйтесь к нашему сообществу разработчиков, зарегистрировавшись сегодня! С нашей интуитивно понятной\n админкой вы можете легко добавить свою игру и получить уникальный ID.\u003c/p\u003e\n \u003ca href=\"/admin/games\" class=\"button\"\u003eПерейти в админку\u003c/a\u003e\n\u003c/section\u003e\n\n\u003csection id=\"download\" class=\"download-section\"\u003e\n \u003cdiv class=\"download-content\"\u003e\n \u003ch3\u003eГотовы к запуску? 🚀\u003c/h3\u003e\n \u003cp\u003eСкачайте нашу админку с GitHub и настройте её на своих серверах без усилий. Присоединяйтесь к нам в\n формировании следующего поколения игр!\u003c/p\u003e\n \u003cbutton class=\"button\" onclick=\"window.open('https://github.com/ascenmmo/multiplayer-game-servers', '_blank');\"\u003e\n Скачать админку с GitHub\n \u003c/button\u003e\n \u003c/div\u003e\n\u003c/section\u003e\n\n\u003cfooter\u003e\n \u003cp\u003e© 2024 Ascenmmo. Все права защищены.\u003c/p\u003e\n\u003c/footer\u003e\n\n\u003cscript\u003e\n function getCookie(name) {\n let matches = document.cookie.match(new RegExp(\n \"(?:^|; )\" + name.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g, '\\\\$1') + \"=([^;]*)\"\n ));\n return matches ? decodeURIComponent(matches[1]) : undefined;\n }\n\n const token = getCookie('token');\n const refreshToken = getCookie('refresh');\n\n const navLinks = document.getElementById('nav-links');\n\n if (token \u0026\u0026 refreshToken) {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/games\"\u003eМои игры\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n `;\n } else {\n navLinks.innerHTML = `\n \u003ca href=\"/admin/auth\"\u003eАвторизоваться\u003c/a\u003e\n \u003ca href=\"/developer/doc\"\u003eДокументация\u003c/a\u003e\n \u003ca href=\"#\" id=\"eng-button\"\u003eEng\u003c/a\u003e\n `;\n }\n\n document.getElementById('eng-button').addEventListener('click', function() {\n document.cookie = \"AdminLanguageLanguage=eng; path=/; max-age=\" + 60*60*24*30;\n window.location.reload(true);\n });\n\n\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n" From 956ba3e88ec6793f9b2699937208bd2935011c64 Mon Sep 17 00:00:00 2001 From: temur abdurahmanov Date: Sun, 10 Nov 2024 05:08:21 +0300 Subject: [PATCH 14/21] set eng html --- .../assets/pages/auth/eng_index.html | 79 +++++++----- .../assets/pages/auth/ru_index.html | 29 +++-- .../pages/developer_info/eng_index.html | 68 +++++----- .../assets/pages/docs/eng_index.html | 51 ++++---- .../assets/pages/docs/ru_index.html | 2 +- .../pages/game_collection/eng_index.html | 81 ++++++------ .../assets/pages/game_info/eng_index.html | 120 +++++++++--------- .../assets/pages/main/eng_index.html | 56 ++++---- pkg/admin_client/assets/pages/pages.go | 16 +-- 9 files changed, 257 insertions(+), 245 deletions(-) diff --git a/pkg/admin_client/assets/pages/auth/eng_index.html b/pkg/admin_client/assets/pages/auth/eng_index.html index 3e36e07..35c9ce6 100644 --- a/pkg/admin_client/assets/pages/auth/eng_index.html +++ b/pkg/admin_client/assets/pages/auth/eng_index.html @@ -3,7 +3,7 @@ - Авторизация | ASCENMMO + Auth | ASCENMMO +

ASCENMMO

-

Информация об Игре

+

Game Info

-

Серверы

- +

Servers

+
-

© 2024 Ascenmmo. Все права защищены.

+

© 2024 Ascenmmo. All rights reserved.