From 4582955da287279cda8566bb78f79493b2b3e598 Mon Sep 17 00:00:00 2001 From: Moonbase Date: Thu, 3 Sep 2026 10:34:22 -0700 Subject: [PATCH] Add project files. --- .dockerignore | 18 + .gitattributes | 63 ++ .gitignore | 7 + LICENSE | 661 ++++++++++++++++++ docker-compose.yml | 21 + run.bat | 3 + run.sh | 4 + sodoff-mmo.sln | 22 + src/Attributes/CommandHandlerAttribute.cs | 10 + .../ExtensionCommandHandlerAttribute.cs | 10 + src/Attributes/ManagementCommandAttribute.cs | 14 + src/CommandHandlers/ChatMessageHandler.cs | 50 ++ src/CommandHandlers/CounterHandler.cs | 27 + src/CommandHandlers/DateTimeHandler.cs | 18 + src/CommandHandlers/EMDZombiesHandler.cs | 29 + src/CommandHandlers/ElapsedTimeSyncHandler.cs | 20 + src/CommandHandlers/ExitHandler.cs | 13 + src/CommandHandlers/GauntletHandlers.cs | 203 ++++++ src/CommandHandlers/GenericMessageHandler.cs | 16 + src/CommandHandlers/HandshakeHandler.cs | 47 ++ src/CommandHandlers/JoinLimboHandler.cs | 17 + src/CommandHandlers/JoinPrivateRoomHandler.cs | 18 + src/CommandHandlers/JoinRoomHandler.cs | 20 + src/CommandHandlers/JoinUserHandler.cs | 32 + src/CommandHandlers/LoginHandler.cs | 96 +++ src/CommandHandlers/LogoutHandler.cs | 22 + src/CommandHandlers/PingHandler.cs | 22 + src/CommandHandlers/PublicMessageHandlers.cs | 35 + src/CommandHandlers/RacingHandlers.cs | 83 +++ src/CommandHandlers/SWRacingHandlers.cs | 344 +++++++++ .../SendMessageBoardHandler.cs | 37 + .../SendMessageReplyHandler.cs | 38 + src/CommandHandlers/SendUserEventHandler.cs | 32 + .../SetPositionVariablesHandler.cs | 77 ++ .../SetUserVariablesHandler.cs | 94 +++ src/CommandHandlers/UDPSNPCommandHandler.cs | 22 + src/CommandHandlers/WorldEventHandlers.cs | 134 ++++ src/Core/ApiWebService.cs | 126 ++++ src/Core/Client.cs | 164 +++++ src/Core/CommandHandler.cs | 8 + src/Core/Configuration.cs | 55 ++ src/Core/GauntletRoom.cs | 130 ++++ src/Core/ModuleManager.cs | 50 ++ src/Core/Racing.cs | 328 +++++++++ src/Core/Room.cs | 160 +++++ src/Core/Runtime.cs | 20 + src/Core/SWRacing.cs | 261 +++++++ src/Core/SpecialRoom.cs | 191 +++++ src/Core/UserBanType.cs | 10 + src/Core/Utils.cs | 56 ++ src/Core/WorldEvent.cs | 285 ++++++++ src/Data/DataDecoder.cs | 55 ++ src/Data/DataEncoder.cs | 132 ++++ src/Data/DataWrapper.cs | 11 + src/Data/NetworkArray.cs | 144 ++++ src/Data/NetworkData.cs | 131 ++++ src/Data/NetworkDataType.cs | 23 + src/Data/NetworkObject.cs | 200 ++++++ src/Data/NetworkPacket.cs | 76 ++ src/Data/PlayerData.cs | 228 ++++++ src/Data/SocketBuffer.cs | 54 ++ src/Dockerfile | 18 + src/Management/AuthenticationInfo.cs | 20 + src/Management/Commands/AnnounceCommand.cs | 15 + src/Management/Commands/BypassCommand.cs | 27 + .../Commands/DisableAllChatsCommand.cs | 13 + src/Management/Commands/DisableChatCommand.cs | 12 + src/Management/Commands/EnableChatCommand.cs | 12 + .../Commands/ListAllChatOverridesCommand.cs | 12 + src/Management/Commands/PlayerCount.cs | 16 + src/Management/Commands/TempMuteCommand.cs | 24 + src/Management/IManagementCommand.cs | 11 + src/Management/ManagementCommandProcessor.cs | 52 ++ src/Program.cs | 22 + src/Server.cs | 165 +++++ src/appsettings.json | 73 ++ src/sodoffmmo.csproj | 21 + 77 files changed, 5790 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 docker-compose.yml create mode 100644 run.bat create mode 100644 run.sh create mode 100644 sodoff-mmo.sln create mode 100644 src/Attributes/CommandHandlerAttribute.cs create mode 100644 src/Attributes/ExtensionCommandHandlerAttribute.cs create mode 100644 src/Attributes/ManagementCommandAttribute.cs create mode 100644 src/CommandHandlers/ChatMessageHandler.cs create mode 100644 src/CommandHandlers/CounterHandler.cs create mode 100644 src/CommandHandlers/DateTimeHandler.cs create mode 100644 src/CommandHandlers/EMDZombiesHandler.cs create mode 100644 src/CommandHandlers/ElapsedTimeSyncHandler.cs create mode 100644 src/CommandHandlers/ExitHandler.cs create mode 100644 src/CommandHandlers/GauntletHandlers.cs create mode 100644 src/CommandHandlers/GenericMessageHandler.cs create mode 100644 src/CommandHandlers/HandshakeHandler.cs create mode 100644 src/CommandHandlers/JoinLimboHandler.cs create mode 100644 src/CommandHandlers/JoinPrivateRoomHandler.cs create mode 100644 src/CommandHandlers/JoinRoomHandler.cs create mode 100644 src/CommandHandlers/JoinUserHandler.cs create mode 100644 src/CommandHandlers/LoginHandler.cs create mode 100644 src/CommandHandlers/LogoutHandler.cs create mode 100644 src/CommandHandlers/PingHandler.cs create mode 100644 src/CommandHandlers/PublicMessageHandlers.cs create mode 100644 src/CommandHandlers/RacingHandlers.cs create mode 100644 src/CommandHandlers/SWRacingHandlers.cs create mode 100644 src/CommandHandlers/SendMessageBoardHandler.cs create mode 100644 src/CommandHandlers/SendMessageReplyHandler.cs create mode 100644 src/CommandHandlers/SendUserEventHandler.cs create mode 100644 src/CommandHandlers/SetPositionVariablesHandler.cs create mode 100644 src/CommandHandlers/SetUserVariablesHandler.cs create mode 100644 src/CommandHandlers/UDPSNPCommandHandler.cs create mode 100644 src/CommandHandlers/WorldEventHandlers.cs create mode 100644 src/Core/ApiWebService.cs create mode 100644 src/Core/Client.cs create mode 100644 src/Core/CommandHandler.cs create mode 100644 src/Core/Configuration.cs create mode 100644 src/Core/GauntletRoom.cs create mode 100644 src/Core/ModuleManager.cs create mode 100644 src/Core/Racing.cs create mode 100644 src/Core/Room.cs create mode 100644 src/Core/Runtime.cs create mode 100644 src/Core/SWRacing.cs create mode 100644 src/Core/SpecialRoom.cs create mode 100644 src/Core/UserBanType.cs create mode 100644 src/Core/Utils.cs create mode 100644 src/Core/WorldEvent.cs create mode 100644 src/Data/DataDecoder.cs create mode 100644 src/Data/DataEncoder.cs create mode 100644 src/Data/DataWrapper.cs create mode 100644 src/Data/NetworkArray.cs create mode 100644 src/Data/NetworkData.cs create mode 100644 src/Data/NetworkDataType.cs create mode 100644 src/Data/NetworkObject.cs create mode 100644 src/Data/NetworkPacket.cs create mode 100644 src/Data/PlayerData.cs create mode 100644 src/Data/SocketBuffer.cs create mode 100644 src/Dockerfile create mode 100644 src/Management/AuthenticationInfo.cs create mode 100644 src/Management/Commands/AnnounceCommand.cs create mode 100644 src/Management/Commands/BypassCommand.cs create mode 100644 src/Management/Commands/DisableAllChatsCommand.cs create mode 100644 src/Management/Commands/DisableChatCommand.cs create mode 100644 src/Management/Commands/EnableChatCommand.cs create mode 100644 src/Management/Commands/ListAllChatOverridesCommand.cs create mode 100644 src/Management/Commands/PlayerCount.cs create mode 100644 src/Management/Commands/TempMuteCommand.cs create mode 100644 src/Management/IManagementCommand.cs create mode 100644 src/Management/ManagementCommandProcessor.cs create mode 100644 src/Program.cs create mode 100644 src/Server.cs create mode 100644 src/appsettings.json create mode 100644 src/sodoffmmo.csproj diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..18759fb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +**/.classpath +**/.env +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4df4dab --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vs +.vscode +.DS_Store +src/bin +src/obj +src/Properties +src/sodoffmmo.csproj.user \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..79bf549 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.8' +services: + sodoffmmo: + build: + context: . + dockerfile: src/Dockerfile + ports: + - "9933:9933" + networks: + - sodoff_network + +networks: + sodoff_network: + name: sodoff_network + # bellow network configuration should be put in at least one file + # - but it may be in many or all + # - without it it will work like `external: true` + driver: bridge + ipam: + config: + - subnet: "172.16.99.0/24" diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..9651b8c --- /dev/null +++ b/run.bat @@ -0,0 +1,3 @@ +dotnet run --project src/sodoffmmo.csproj + +pause diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..835f5b9 --- /dev/null +++ b/run.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd "$(dirname "$0")" +dotnet run --project src/sodoffmmo.csproj diff --git a/sodoff-mmo.sln b/sodoff-mmo.sln new file mode 100644 index 0000000..f56f5a4 --- /dev/null +++ b/sodoff-mmo.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sodoffmmo", "src\sodoffmmo.csproj", "{049E36E9-29EC-4A15-96A5-99A049BD2F83}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {049E36E9-29EC-4A15-96A5-99A049BD2F83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {049E36E9-29EC-4A15-96A5-99A049BD2F83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {049E36E9-29EC-4A15-96A5-99A049BD2F83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {049E36E9-29EC-4A15-96A5-99A049BD2F83}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Attributes/CommandHandlerAttribute.cs b/src/Attributes/CommandHandlerAttribute.cs new file mode 100644 index 0000000..c7adc7d --- /dev/null +++ b/src/Attributes/CommandHandlerAttribute.cs @@ -0,0 +1,10 @@ +namespace sodoffmmo.Attributes; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +class CommandHandlerAttribute : Attribute { + public int ID { get; } + + public CommandHandlerAttribute(int id) { + ID = id; + } +} \ No newline at end of file diff --git a/src/Attributes/ExtensionCommandHandlerAttribute.cs b/src/Attributes/ExtensionCommandHandlerAttribute.cs new file mode 100644 index 0000000..b50eef3 --- /dev/null +++ b/src/Attributes/ExtensionCommandHandlerAttribute.cs @@ -0,0 +1,10 @@ +namespace sodoffmmo.Attributes; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +class ExtensionCommandHandlerAttribute : Attribute { + public string Name { get; } + + public ExtensionCommandHandlerAttribute(string name) { + Name = name; + } +} \ No newline at end of file diff --git a/src/Attributes/ManagementCommandAttribute.cs b/src/Attributes/ManagementCommandAttribute.cs new file mode 100644 index 0000000..561ce2e --- /dev/null +++ b/src/Attributes/ManagementCommandAttribute.cs @@ -0,0 +1,14 @@ +using sodoffmmo.Management; + +namespace sodoffmmo.Attributes; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public class ManagementCommandAttribute : Attribute { + public string Name { get; set; } + public Role Role { get; set; } + + public ManagementCommandAttribute(string name, Role role) { + Name = name; + Role = role; + } +} diff --git a/src/CommandHandlers/ChatMessageHandler.cs b/src/CommandHandlers/ChatMessageHandler.cs new file mode 100644 index 0000000..ffdacf7 --- /dev/null +++ b/src/CommandHandlers/ChatMessageHandler.cs @@ -0,0 +1,50 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; +using sodoffmmo.Management; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("SCM")] +class ChatMessageHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + string message = receivedObject.Get("p").Get("chm"); + if (ManagementCommandProcessor.ProcessCommand(message, client)) + return Task.CompletedTask; + if (client.TempMuted) { + ClientMuted(client); + return Task.CompletedTask; + } + if (!Configuration.ServerConfiguration.EnableChat && !client.Room.AllowChatOverride) { + ChatDisabled(client); + } else { + Chat(client, message); + } + return Task.CompletedTask; + } + + public void ChatDisabled(Client client) { + client.Send(Utils.BuildServerSideMessage("Unfortunately, chat has been disabled by server administrators", "Server")); + } + + public void ClientMuted(Client client) { + client.Send(Utils.BuildServerSideMessage("You have been muted by the moderators", "Server")); + } + + public void Chat(Client client, string message) { + if (Configuration.ServerConfiguration.Authentication >= AuthenticationMode.RequiredForChat && client.PlayerData.DiplayName == "placeholder") { + client.Send(Utils.BuildServerSideMessage("You must be authenticated to use the chat", "Server")); + return; + } + + client.Room.Send(Utils.BuildChatMessage(client.PlayerData.Uid, message, client.PlayerData.DiplayName), client); + + NetworkObject cmd = new(); + NetworkObject data = new(); + data.Add("arr", new string[] { "SCA", "-1", "1", message, "", "1" }); + cmd.Add("c", "SCA"); + cmd.Add("p", data); + NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); + client.Send(packet); + } +} diff --git a/src/CommandHandlers/CounterHandler.cs b/src/CommandHandlers/CounterHandler.cs new file mode 100644 index 0000000..1737041 --- /dev/null +++ b/src/CommandHandlers/CounterHandler.cs @@ -0,0 +1,27 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("SCE")] +class CounterEventHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { // {"a":13,"c":1,"p":{"c":"SCE","p":{"NAME":"COUNT"},"r":-1}} + if (client.Room is SpecialRoom room) { + string name = receivedObject.Get("p").Get("NAME"); + if (name == "COUNT" || name == "COUNT2" || name == "COUNT3") { + int index = name switch { + "COUNT" => 0, + "COUNT2" => 1, + "COUNT3" => 2 + }; + room.ambassadorGauges[index] = Math.Min(100, room.ambassadorGauges[index]+(1/Configuration.ServerConfiguration.AmbassadorGaugePlayers)); + room.Send(Utils.VlNetworkPacket(room.GetRoomVars(), client.Room.Id)); + } else { + Console.WriteLine($"Invalid attempt to increment room var {name} in {room.Name}."); + } + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/CommandHandlers/DateTimeHandler.cs b/src/CommandHandlers/DateTimeHandler.cs new file mode 100644 index 0000000..5bdf459 --- /dev/null +++ b/src/CommandHandlers/DateTimeHandler.cs @@ -0,0 +1,18 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("DT")] +class DateTimeHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + NetworkObject cmd = new(); + NetworkObject obj = new(); + obj.Add("arr", new string[] { "DT", DateTime.UtcNow.ToString("MM/dd/yyyy HH:mm:ss") }); + cmd.Add("c", "DT"); + cmd.Add("p", obj); + client.Send(NetworkObject.WrapObject(1, 13, cmd).Serialize()); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/EMDZombiesHandler.cs b/src/CommandHandlers/EMDZombiesHandler.cs new file mode 100644 index 0000000..418a11d --- /dev/null +++ b/src/CommandHandlers/EMDZombiesHandler.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +// TODO: This is currently stubbed. Supposed to do something. +// Should probably be done by someone who actually played the game. +[ExtensionCommandHandler("SU")] +public class EMDZombiesUpdateHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("EN")] +public class EMDZombiesEnterHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("EX")] +public class EMDZombiesExitHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/ElapsedTimeSyncHandler.cs b/src/CommandHandlers/ElapsedTimeSyncHandler.cs new file mode 100644 index 0000000..3311d5c --- /dev/null +++ b/src/CommandHandlers/ElapsedTimeSyncHandler.cs @@ -0,0 +1,20 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("RTM")] +class ElapsedTimeSyncHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + if (client.Room != null) { + NetworkObject cmd = new(); + NetworkObject obj = new(); + obj.Add("arr", new string[] { "RTM", "-1", Runtime.CurrentRuntime.ToString() }); + cmd.Add("c", "RTM"); + cmd.Add("p", obj); + client.Send(NetworkObject.WrapObject(1, 13, cmd).Serialize()); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/CommandHandlers/ExitHandler.cs b/src/CommandHandlers/ExitHandler.cs new file mode 100644 index 0000000..1154345 --- /dev/null +++ b/src/CommandHandlers/ExitHandler.cs @@ -0,0 +1,13 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[CommandHandler(26)] +class ExitHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + client.SetRoom(null); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/GauntletHandlers.cs b/src/CommandHandlers/GauntletHandlers.cs new file mode 100644 index 0000000..118756c --- /dev/null +++ b/src/CommandHandlers/GauntletHandlers.cs @@ -0,0 +1,203 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +using System.Timers; + +namespace sodoffmmo.CommandHandlers; + + +// Host Room For Any +[ExtensionCommandHandler("gs.HRFA")] +class GauntletCreateRoomHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + GauntletRoom.Join(client); + return Task.CompletedTask; + } +} + +// Join Any Room +[ExtensionCommandHandler("gs.JAR")] +class GauntletJoinRoomHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + GauntletRoom.Join(client); + return Task.CompletedTask; + } +} + +// Play Again +[ExtensionCommandHandler("gs.PA")] +class GauntletPlayAgainHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + GauntletRoom room = (client.Room as GauntletRoom)!; + room.SetPlayerReady(client, false); + + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "LUNR", + room.Id.ToString(), + client.PlayerData.Uid + }, "msg", room.Id); + + room.Send(packet, client); + room.SendPA(client); + return Task.CompletedTask; + } +} + +// Lobby User Ready +[ExtensionCommandHandler("gs.LUR")] +class GauntletLobbyUserReadyHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + GauntletRoom room = (client.Room as GauntletRoom)!; + room.SetPlayerReady(client); + + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "LUR", + room.Id.ToString(), + client.PlayerData.Uid + }, "msg", room.Id); + + room.Send(packet); + + if (room.GetReadyCount() > 1) { + packet = Utils.ArrNetworkPacket(new string[] { + "LCDD", // Lobby CountDown Done + room.Id.ToString(), + client.PlayerData.Uid + }, "msg", room.Id); + + room.Send(packet); + } + return Task.CompletedTask; + } +} + +// Lobby User Not Ready +[ExtensionCommandHandler("gs.LUNR")] +class GauntletLobbyUserNotReadyHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + GauntletRoom room = (client.Room as GauntletRoom)!; + room.SetPlayerReady(client, false); + + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "LUNR", + room.Id.ToString(), + client.PlayerData.Uid + }, "msg", room.Id); + + room.Send(packet); + return Task.CompletedTask; + } +} + +// Game Level Load +[ExtensionCommandHandler("gs.GLL")] +class GauntletLevelLoadHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { // {"a":13,"c":1,"p":{"c":"gs.GLL","p":{"0":"0","1":"0","2":"5","en":"GauntletGameExtension"},"r":365587}} + GauntletRoom room = (client.Room as GauntletRoom)!; + NetworkObject p = receivedObject.Get("p"); + + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "GLL", // Game CountDown Start + room.Id.ToString(), + p.Get("0"), + p.Get("1"), + p.Get("2") // TODO use size of p.fields - 1 + }, "msg", room.Id); + room.Send(packet); + return Task.CompletedTask; + } +} + +// Game Level Loaded +[ExtensionCommandHandler("gs.GLLD")] +class GauntletLevelLoadedHandler : CommandHandler +{ + private System.Timers.Timer? timer = null; + private int counter; + private GauntletRoom room; + + public override Task Handle(Client client, NetworkObject receivedObject) { + room = (client.Room as GauntletRoom)!; + counter = 5; + + // {"a":13,"c":1,"p":{"c":"msg","p":{"arr":["GCDS","365587","4"]},"r":365587}} + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "GCDS", // Game CountDown Start + room.Id.ToString(), + (--counter).ToString() + }, "msg", room.Id); + room.Send(packet); + + timer = new System.Timers.Timer(1500); + timer.AutoReset = true; + timer.Enabled = true; + timer.Elapsed += OnTick; + return Task.CompletedTask; + } + + private void OnTick(Object? source, ElapsedEventArgs e) { + NetworkPacket packet; + if (--counter > 0) { + // {"a":13,"c":1,"p":{"c":"msg","p":{"arr":["GCDS","365587","4"]},"r":365587}} + packet = Utils.ArrNetworkPacket(new string[] { + "GCDU", // Game CountDown Update + room.Id.ToString(), + counter.ToString() + }, "msg", room.Id); + } else { + // {"a":13,"c":1,"p":{"c":"msg","p":{"arr":["GS","365587"]},"r":365587}} + packet = Utils.ArrNetworkPacket(new string[] { + "GS", // Game Start + room.Id.ToString() + }, "msg", room.Id); + + timer!.Stop(); + timer!.Close(); + timer = null; + } + room.Send(packet); + } +} + +// Relay Game Data +[ExtensionCommandHandler("gs.RGD")] +class GauntletRelayGameDataHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) // {"a":13,"c":1,"p":{"c":"gs.RGD","p":{"0":"2700","1":"78","en":"GauntletGameExtension"},"r":4}} + { + GauntletRoom room = (client.Room as GauntletRoom)!; + NetworkObject p = receivedObject.Get("p"); + + // {"a":13,"c":1,"p":{"c":"msg","p":{"arr":["RGD","365587","150","75"]},"r":365587}} + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "RGD", // Relay Game Data + room.Id.ToString(), + p.Get("0"), + p.Get("1") + }, "msg", room.Id); + room.Send(packet, client); + return Task.CompletedTask; + } +} + +// Game Complete +[ExtensionCommandHandler("gs.GC")] +class GauntletGameCompleteHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) // {"a":13,"c":1,"p":{"c":"gs.GC","p":{"0":"1550","1":"84","en":"GauntletGameExtension"},"r":4}} + { + GauntletRoom room = (client.Room as GauntletRoom)!; + NetworkObject p = receivedObject.Get("p"); + + room.ProcessResult(client, p.Get("0"), p.Get("1")); + return Task.CompletedTask; + } +} + diff --git a/src/CommandHandlers/GenericMessageHandler.cs b/src/CommandHandlers/GenericMessageHandler.cs new file mode 100644 index 0000000..37f9a93 --- /dev/null +++ b/src/CommandHandlers/GenericMessageHandler.cs @@ -0,0 +1,16 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[CommandHandler(7)] +class GenericMessageHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + if (!Configuration.ServerConfiguration.EnableCannedChat && receivedObject.Get("m").StartsWith("C:")) + return Task.CompletedTask; + NetworkPacket packet = NetworkObject.WrapObject(0, 7, receivedObject).Serialize(); + client.Room.Send(packet); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/HandshakeHandler.cs b/src/CommandHandlers/HandshakeHandler.cs new file mode 100644 index 0000000..ab34487 --- /dev/null +++ b/src/CommandHandlers/HandshakeHandler.cs @@ -0,0 +1,47 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; +using System.Text; +using System; + +namespace sodoffmmo.CommandHandlers; + +[CommandHandler(0)] +class HandshakeHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) + { + string? token = receivedObject.Get("rt"); + if (token != null) { + client.Send(NetworkObject.WrapObject(0, 1006, new NetworkObject()).Serialize()); + return Task.CompletedTask; + } + + string? api = receivedObject.Get("api"); + if (api != null && api[0] == '0') { + client.OldApi = true; + } + + NetworkObject obj = new(); + + obj.Add("tk", RandomString(32)); + obj.Add("ct", 1024); + obj.Add("ms", 1000000); + + client.Send(NetworkObject.WrapObject(0, 0, obj).Serialize()); + return Task.CompletedTask; + } + + private string RandomString(int length) { + Random random = new Random(); + const string pool = "abcdefghijklmnopqrstuvwxyz0123456789"; + var builder = new StringBuilder(); + + for (var i = 0; i < length; i++) { + var c = pool[random.Next(0, pool.Length)]; + builder.Append(c); + } + + return builder.ToString(); + } +} diff --git a/src/CommandHandlers/JoinLimboHandler.cs b/src/CommandHandlers/JoinLimboHandler.cs new file mode 100644 index 0000000..ca4554f --- /dev/null +++ b/src/CommandHandlers/JoinLimboHandler.cs @@ -0,0 +1,17 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers +{ + [ExtensionCommandHandler("JL")] + public class JoinLimboHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + client.SetRoom(Room.GetOrAdd("LIMBO")); + + return Task.CompletedTask; + } + } +} diff --git a/src/CommandHandlers/JoinPrivateRoomHandler.cs b/src/CommandHandlers/JoinPrivateRoomHandler.cs new file mode 100644 index 0000000..bd9d5cb --- /dev/null +++ b/src/CommandHandlers/JoinPrivateRoomHandler.cs @@ -0,0 +1,18 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("JO")] +class JoinPrivateRoomHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) + { + var p = receivedObject.Get("p"); + string roomName = p.Get("rn") + "_" + p.Get("0"); + Room room = Room.GetOrAdd(roomName, autoRemove: true); + client.SetRoom(room); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/JoinRoomHandler.cs b/src/CommandHandlers/JoinRoomHandler.cs new file mode 100644 index 0000000..e633ae4 --- /dev/null +++ b/src/CommandHandlers/JoinRoomHandler.cs @@ -0,0 +1,20 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("JA")] +class JoinRoomHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) + { + string roomName = receivedObject.Get("p").Get("rn"); + if (roomName is null) { + roomName = client.PlayerData.ZoneName; + } + Room room = Room.GetOrAdd(roomName); + client.SetRoom(room); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/JoinUserHandler.cs b/src/CommandHandlers/JoinUserHandler.cs new file mode 100644 index 0000000..512842d --- /dev/null +++ b/src/CommandHandlers/JoinUserHandler.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers +{ + [ExtensionCommandHandler("JU")] + public class JoinUserHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + string mpId = receivedObject.Get("p").Get("0"); + + if (mpId != null) + { + Room? room = Room.AllRooms().FirstOrDefault(e => e.Id == Int32.Parse(mpId)); + + if (room != null) + { + client.SetRoom(room); + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/CommandHandlers/LoginHandler.cs b/src/CommandHandlers/LoginHandler.cs new file mode 100644 index 0000000..0714226 --- /dev/null +++ b/src/CommandHandlers/LoginHandler.cs @@ -0,0 +1,96 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; +using sodoffmmo.Management; + +namespace sodoffmmo.CommandHandlers; + +[CommandHandler(1)] +class LoginHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) + { + client.PlayerData.UNToken = receivedObject.Get("un"); + client.PlayerData.ZoneName = receivedObject.Get("zn"); + + if (!ValidToken(client)) { + NetworkObject obj = new(); + obj.Add("dr", (byte)1); + client.Send(NetworkObject.WrapObject(0, 1005, obj).Serialize()); + client.ScheduleDisconnect(); + return Task.CompletedTask; + } + + NetworkArray rl = new(); + + NetworkArray r1 = new(); + r1.Add(0); + r1.Add("MP_SYS"); + r1.Add("default"); + r1.Add(true); + r1.Add(false); + r1.Add(false); + r1.Add((short)0); + r1.Add((short)10); + r1.Add(new NetworkArray()); + r1.Add((short)0); + r1.Add((short)0); + rl.Add(r1); + + NetworkArray r2 = new(); + r2.Add(1); + r2.Add("ADMIN"); + r2.Add("default"); + r2.Add(false); + r2.Add(false); + r2.Add(true); + r2.Add((short)0); + r2.Add((short)1); + r2.Add(WorldEvent.Get().EventInfoArray(true)); + rl.Add(r2); + + NetworkObject content = new(); + content.Add("rl", rl); + content.Add("zn", client.PlayerData.ZoneName); + content.Add("rs", (short)5); + content.Add("un", client.PlayerData.UNToken); + content.Add("id", client.ClientID); + content.Add("pi", (short)1); + + client.Send(NetworkObject.WrapObject(0, 1, content).Serialize()); + return Task.CompletedTask; + } + + private bool ValidToken(Client client) { + if (Configuration.ServerConfiguration.Authentication == AuthenticationMode.Disabled || + (client.PlayerData.UNToken == Configuration.ServerConfiguration.BypassToken && !string.IsNullOrEmpty(Configuration.ServerConfiguration.BypassToken))) + return true; + + try { + HttpClient httpClient = new(); + var content = new FormUrlEncodedContent( + new Dictionary { + { "token", client.PlayerData.UNToken }, + }); + + httpClient.Timeout = new TimeSpan(0, 0, 3); + var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Authentication/MMOAuthentication", content).Result; + string? responseString = response.Content.ReadAsStringAsync().Result; + + if (response.StatusCode != System.Net.HttpStatusCode.OK) + throw new Exception($"Response status code {response.StatusCode}"); + if (responseString == null) + throw new Exception("Response string null"); + + AuthenticationInfo info = Utils.DeserializeXml(responseString); + if (info.Authenticated) { + client.PlayerData.DiplayName = info.DisplayName; + client.PlayerData.Role = info.Role; + return true; + } + } catch (Exception ex) { + Console.WriteLine($"Authentication exception IID: {client.ClientID} - {ex}"); + } + return Configuration.ServerConfiguration.Authentication != AuthenticationMode.Required; // return true on auth err if not Required mode + } +} diff --git a/src/CommandHandlers/LogoutHandler.cs b/src/CommandHandlers/LogoutHandler.cs new file mode 100644 index 0000000..85180f2 --- /dev/null +++ b/src/CommandHandlers/LogoutHandler.cs @@ -0,0 +1,22 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[CommandHandler(2)] +class LogoutHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) + { + client.SetRoom(null); + client.PlayerData.UNToken = null; + client.PlayerData.ZoneName = null; + + NetworkObject content = new(); + content.Add("zn", ""); + + client.Send(NetworkObject.WrapObject(0, 2, content).Serialize()); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/PingHandler.cs b/src/CommandHandlers/PingHandler.cs new file mode 100644 index 0000000..cfc665a --- /dev/null +++ b/src/CommandHandlers/PingHandler.cs @@ -0,0 +1,22 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("PNG")] +class PingHandler : CommandHandler { + public bool RunInBackground { get; } = true; + + public override async Task Handle(Client client, NetworkObject receivedObject) { + if (Configuration.ServerConfiguration.PingDelay > 0) { + await Task.Delay(Configuration.ServerConfiguration.PingDelay); + } + NetworkObject cmd = new(); + NetworkObject obj = new(); + obj.Add("arr", new string[] { "PNG", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString() }); + cmd.Add("c", "PNG"); + cmd.Add("p", obj); + client.Send(NetworkObject.WrapObject(1, 13, cmd).Serialize()); + } +} diff --git a/src/CommandHandlers/PublicMessageHandlers.cs b/src/CommandHandlers/PublicMessageHandlers.cs new file mode 100644 index 0000000..178749e --- /dev/null +++ b/src/CommandHandlers/PublicMessageHandlers.cs @@ -0,0 +1,35 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("PM")] +class RacingPMHandler : CommandHandler +{ + // rec: {"a":13,"c":1,"p":{"c":"PM","p":{"M":"DT:c4647597-a72a-4f34-973c-5a10218d9a64:1000","en":"we"},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + // send: {"a":13,"c":1,"p":{"c":"PM","p":{"arr":[{"M":["DT:f05fc387-7358-4bff-be04-7c316f0a8de8:1000"],"MID":3529441}]}}} + NetworkObject cmd = new(); + NetworkObject p = new(); + NetworkArray arr = new(); + NetworkObject data = new(); + string M = receivedObject.Get("p").Get("M"); + if (M.StartsWith("WF:") || M.StartsWith("WFWD:")) { + // When firing weapon in EMD, recieving clients expect userid, but the sending client sends its token instead. + string token = M.Split(':')[1]; + M = M.Replace(token, client.PlayerData.Uid); + } + data.Add("M", new string[] {M}); + data.Add("MID", client.ClientID); + arr.Add(data); + p.Add("arr", arr); + cmd.Add("c", "PM"); + cmd.Add("p", p); + NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); + + if (client.Room != null) // Throws an exception in Eat my Dust when the player fires their weapon before fully in the room. + client.Room.Send(packet); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/RacingHandlers.cs b/src/CommandHandlers/RacingHandlers.cs new file mode 100644 index 0000000..57a1d89 --- /dev/null +++ b/src/CommandHandlers/RacingHandlers.cs @@ -0,0 +1,83 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +using System.Timers; + +namespace sodoffmmo.CommandHandlers; + +// Set Player Ready +[ExtensionCommandHandler("dr.PR")] +class RacingPlayerReadyHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { // {"a":13,"c":1,"p":{"c":"dr.PR","p":{"IMR":"True","en":""},"r":-1}} + NetworkObject p = receivedObject.Get("p"); + RacingPlayerState ready = p.Get("IMR") == "True" ? RacingPlayerState.Ready : RacingPlayerState.NotReady; + + // client send also {"a":7,"c":0,"p":{"m":"IMR:ae70ef16-c52c-43e4-9305-d6ea3e378a0d:True","r":412456,"t":0,"u":3529456}} + // so server do not need generate this packet + + if (client.Room.Group == "RacingDragon") { + RacingRoom room = (client.Room as RacingRoom)!; + room.SetPlayerState(client, ready); + Console.WriteLine($"IMR Lobby: {client.ClientID} {ready}"); + room.TryLoad(); + } else { + RacingLobby.Lobby.SetPlayerState(client, ready); + Console.WriteLine($"IMR: {client.ClientID} {ready}"); + } + + return Task.CompletedTask; + } +} + +// Player Status Request +[ExtensionCommandHandler("dr.PS")] +class RacingPlayerStatusHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + client.Send(RacingLobby.Lobby.GetPS()); + return Task.CompletedTask; + } +} + +// User Ready ACK +[ExtensionCommandHandler("dr.UACK")] +class RacingUACKHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + RacingRoom room = (client.Room as RacingRoom)!; + room.SetPlayerState(client, RacingPlayerState.RaceReady1); + return Task.CompletedTask; + } +} + +// All Ready ACK +[ExtensionCommandHandler("dr.ARACK")] +class RacingARACKHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { + RacingRoom room = (client.Room as RacingRoom)!; + room.SetPlayerState(client, RacingPlayerState.RaceReady2); + + if (room.GetPlayersCount(RacingPlayerState.RaceReady2) == room.ClientsCount) { + NetworkPacket packet = room.GetSTAPacket(); + room.Send(packet); + Console.WriteLine($"STA"); + } + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("dr.AR")] +class RacingARHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) { // {"a":13,"c":1,"p":{"c":"dr.AR","p":{"CT":"112.1268","FD":"3008.283","LC":"3","UN":"scourgexxwulf","en":""},"r":412467}} + RacingRoom room = (client.Room as RacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + + room.SetResults(client, p.Get("UN"), p.Get("CT"), p.Get("LC")); + room.SendResults(); + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/SWRacingHandlers.cs b/src/CommandHandlers/SWRacingHandlers.cs new file mode 100644 index 0000000..96e7059 --- /dev/null +++ b/src/CommandHandlers/SWRacingHandlers.cs @@ -0,0 +1,344 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; + +namespace sodoffmmo.CommandHandlers +{ + [ExtensionCommandHandler("sl.AV")] + class SWCreateRoomHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = new(); + + client.SetRoom(room); + + room.AddPlayer(client); + room.SetIsPrebuiltTrack(client, true); + room.SetTrack(client, -9999); + + room.Send(room.GetAMPacket()); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("sl.AU")] + class SWHostWithBuddysHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + // more than likely its the same as AV except flagged as a BuddiesOnly room lol + + SWRacingRoom room = new() + { + BuddiesOnly = true + }; + + client.SetRoom(room); + + room.AddPlayer(client); + room.SetIsPrebuiltTrack(client, true); + room.SetTrack(client, -9999); + + room.Send(room.GetAMPacket()); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("sl.AT")] + class SWJoinBuddyHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + NetworkObject p = receivedObject.Get("p"); + string[] req = p.Get("AT").Split("|"); + + Client? host = Server.AllClients?.FirstOrDefault(e => e.PlayerData.Uid == req[0]); + if(host?.Room is not SWRacingRoom room) + { + client.Send(Utils.ArrNetworkPacket(["ZV", "1"])); + + return Task.CompletedTask; + } + + client.SetRoom(room); + room.AddPlayer(client); + room.Send(room.GetAMPacket()); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("sl.AQ")] + class SWJoinRoomHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + foreach (var room in Room.AllRooms()) + { + if (room is SWRacingRoom swRoom) + { + if (swRoom!.BuddiesOnly) continue; + if (swRoom!.RaceOngoing) continue; + else if (swRoom!.ClientsCount >= 4) continue; + else + { + client.SetRoom(room); + + swRoom.AddPlayer(client); + room.Send(swRoom.GetAMPacket()); + + return Task.CompletedTask; + } + } + } + + // if no rooms are available, treat it like AV and just make a room + + SWRacingRoom racingRoom = new(); + client.SetRoom(racingRoom); + + racingRoom.AddPlayer(client); + racingRoom.SetIsPrebuiltTrack(client, true); + racingRoom.SetTrack(client, -9999); + + racingRoom.Send(racingRoom.GetAMPacket()); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.AN")] + class SWReadyHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + room.SetReady(client, true); + + room.Send(Utils.ArrNetworkPacket([$"AN|{room.GetIndex(client)}"])); + + room.TryStartLobbyCountdown(); + if (room.CountdownStarted) + room.Send(Utils.ArrNetworkPacket(["AD"], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.AE")] + class SWTransformRelayHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + string[] fields = p.Get("ZC").Split('|'); + + room.SetPlayerPosition(client, float.Parse(fields[1]), float.Parse(fields[2]), float.Parse(fields[3])); + + List outArr = [ room.GetIndex(client).ToString() ]; + outArr.AddRange(fields[1..]); + outArr.Add(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); + + outArr.Insert(0, "AE"); + string joined = string.Join('|', outArr); + + room.Send(Utils.ArrNetworkPacket([joined], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.AG")] + class SWProjectileTransformRelayHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + string[] fields = p.Get("AG").Split("|"); + + List outArr = [room.GetIndex(client).ToString()]; + outArr.AddRange(fields[1..]); + outArr.Add(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); + + outArr.Insert(0, "AG"); + string joined = string.Join('|', outArr); + + room.Send(Utils.ArrNetworkPacket([joined], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.AF")] + class SWProjectileDestroyedHandler : CommandHandler + { + const float hitRadius = 5f; // calibrate me if to OP + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + string[] fields = p.Get("AF").Split("|"); + + Vector3 blast = new(float.Parse(fields[1]), float.Parse(fields[2]), float.Parse(fields[3])); + + List outFields = ["AF", room.GetIndex(client).ToString(), fields[0], fields[1], fields[2], fields[3]]; + outFields.AddRange(room.GetPlayersWithinRadius(blast, hitRadius, client).Select(idx => idx.ToString())); + + room.Send(Utils.ArrNetworkPacket([string.Join("|", outFields)], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.AC")] + class SWSceneLoadedHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + if (room.SetSceneLoaded(client)) + room.Send(Utils.ArrNetworkPacket(["AD"], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.ZI")] + class SWNotReadyHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + room.SetReady(client, false); + + room.Send(Utils.ArrNetworkPacket([$"ZI|{room.GetIndex(client)}"])); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.ZG")] + class SWUpdateBoatHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + string packed = p.Get("ZG"); + string[] unpacked = packed.Split('|'); + + room.SetBoat(client, unpacked[1], int.Parse(unpacked[0])); + + string joined = string.Join("|", "ZG", room.GetIndex(client).ToString(), unpacked[0], unpacked[1]); + room.Send(Utils.ArrNetworkPacket([joined], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.ZL")] + class SWUpdateTrackHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + string packed = p.Get("ZL"); + string[] unpacked = packed.Split('|'); + + room.SetTrack(client, int.Parse(unpacked[1])); + room.SetIsPrebuiltTrack(client, unpacked[0] == "ZP"); + room.SetTrackStatus(client, unpacked[2]); + + string joined = string.Join("|", "ZL", room.GetIndex(client).ToString(), unpacked[0], unpacked[1], unpacked[2]); + room.Send(Utils.ArrNetworkPacket([joined], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.ZU")] + class SWUpdateUsernameHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + string packed = p.Get("ZU"); + + var uid = packed.Split("|")[0]; + room.SetToken(client, uid); + + string joined = string.Join("|", "ZU", room.GetIndex(client).ToString(), uid); + room.Send(Utils.ArrNetworkPacket([joined], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.ZB")] + class SWCountdownDoneHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + if (room.SetCountdownFinished(client)) + room.Send(Utils.ArrNetworkPacket(["ZF"], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.ZD")] + class SWLapPlaceUpdateHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + NetworkObject p = receivedObject.Get("p"); + string[] unpacked = p.Get("ZD").Split('|'); + + room.SetPlayerLap(client, int.Parse(unpacked[0])); + + if (unpacked.Length == 2) + room.SetFinishedTime(client, float.Parse(unpacked[1])); + + string joined = string.Join("|", "ZD", room.GetIndex(client).ToString(), unpacked[0]); + + if (unpacked.Length == 2) + joined += "|" + unpacked[1]; + + room.Send(Utils.ArrNetworkPacket([joined], "", room.Id)); + + return Task.CompletedTask; + } + } + + [ExtensionCommandHandler("swl.ZW")] + class SWPlayAgainHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + SWRacingRoom room = (client.Room as SWRacingRoom)!; + + room.RaceOngoing = false; + room.CountdownStarted = false; + + // probably need to do something to reset player states also but we'll worry about that later, send it + room.Send(Utils.ArrNetworkPacket(["ZW"])); + + return Task.CompletedTask; + } + } +} diff --git a/src/CommandHandlers/SendMessageBoardHandler.cs b/src/CommandHandlers/SendMessageBoardHandler.cs new file mode 100644 index 0000000..fd383b5 --- /dev/null +++ b/src/CommandHandlers/SendMessageBoardHandler.cs @@ -0,0 +1,37 @@ +using System; +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("SMB")] +class SendMessageBoardHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) + { + NetworkObject args = receivedObject.Get("p"); + + string toUserId = args.Get("tgt"); + string content = args.Get("cnt"); + string level = args.Get("lvl"); + + if (toUserId == string.Empty) toUserId = client.PlayerData.Uid; // send to self + + ApiWebService apiWebService = new(); + + // first check for any kind of ban + var banType = apiWebService.CheckForUserBan(client); + + if (banType != null && banType >= UserBanType.TemporaryOpenChatBan) { client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF")); return Task.CompletedTask; } + + // send message + var result = apiWebService.SendMessageBoard(client, toUserId, content, level, "0"); + + if (result) + client.Send(Utils.ArrNetworkPacket(new string[] { "SMA", "-1", "SUCCESS", "1", DateTime.UtcNow.ToString() }, "SMA")); + else client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF")); + + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/SendMessageReplyHandler.cs b/src/CommandHandlers/SendMessageReplyHandler.cs new file mode 100644 index 0000000..781a804 --- /dev/null +++ b/src/CommandHandlers/SendMessageReplyHandler.cs @@ -0,0 +1,38 @@ +using System; +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("SMR")] +class SendMessageReplyHandler : CommandHandler +{ + public override Task Handle(Client client, NetworkObject receivedObject) + { + NetworkObject args = receivedObject.Get("p"); + + string toUserId = args.Get("tgt"); + string content = args.Get("cnt"); + string level = args.Get("lvl"); + string msgId = args.Get("rtm"); + + if (toUserId == string.Empty) toUserId = client.PlayerData.Uid; // send to self + + ApiWebService apiWebService = new(); + + // first check for any kind of ban + var banType = apiWebService.CheckForUserBan(client); + + if (banType != null && banType >= UserBanType.TemporaryOpenChatBan) { client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF")); return Task.CompletedTask; } + + // send message + var result = apiWebService.SendMessageBoard(client, toUserId, content, level, msgId); + + if (result) + client.Send(Utils.ArrNetworkPacket(new string[] { "SMA", "-1", "SUCCESS", "1", DateTime.UtcNow.ToString() }, "SMA")); + else client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF")); + + return Task.CompletedTask; + } +} diff --git a/src/CommandHandlers/SendUserEventHandler.cs b/src/CommandHandlers/SendUserEventHandler.cs new file mode 100644 index 0000000..e957baa --- /dev/null +++ b/src/CommandHandlers/SendUserEventHandler.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers +{ + [ExtensionCommandHandler("SUE")] + public class SendUserEventHandler : CommandHandler + { + public override Task Handle(Client client, NetworkObject receivedObject) + { + NetworkObject p = receivedObject.Get("p"); + + string userId = p.Get("UID"); + string cmd = p.Get("CMD"); + string[] arr = p.Get("ARR"); + + // find client in all clients list + Client? client1 = Server.AllClients?.FirstOrDefault(e => e.PlayerData.Uid == userId); + + // send command + if (client1 != null) client1.Send(Utils.ArrNetworkPacket(arr, cmd)); + + return Task.CompletedTask; + } + } +} diff --git a/src/CommandHandlers/SetPositionVariablesHandler.cs b/src/CommandHandlers/SetPositionVariablesHandler.cs new file mode 100644 index 0000000..f9a5d45 --- /dev/null +++ b/src/CommandHandlers/SetPositionVariablesHandler.cs @@ -0,0 +1,77 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("SPV")] +class SetPositionVariablesHandler : CommandHandler { + Client client; + NetworkObject spvData; + public override Task Handle(Client client, NetworkObject receivedObject) { + if (client.Room == null) { + Console.WriteLine($"SPV Missing Room IID: {client.ClientID}"); + client.Send(NetworkObject.WrapObject(0, 1006, new NetworkObject()).Serialize()); + client.ScheduleDisconnect(); + return Task.CompletedTask; + } + this.client = client; + spvData = receivedObject.Get("p"); + UpdatePositionVariables(); + SendSPVCommand(); + + return Task.CompletedTask; + } + + private void UpdatePositionVariables() { + float[] pos = spvData.Get("U"); + client.PlayerData.R = spvData.Get("R"); + client.PlayerData.P1 = pos[0]; + client.PlayerData.P2 = pos[1]; + client.PlayerData.P3 = pos[2]; + client.PlayerData.R1 = pos[3]; + client.PlayerData.R2 = pos[4]; + client.PlayerData.R3 = pos[5]; + client.PlayerData.Mx = spvData.Get("MX"); + client.PlayerData.F = spvData.Get("F"); + client.PlayerData.Mbf = spvData.Get("MBF"); + } + + private void SendSPVCommand() { + NetworkObject cmd = new(); + NetworkObject obj = new(); + NetworkArray container = new(); + NetworkObject vars = new(); + vars.Add("R", client.PlayerData.R); + vars.Add("U", new float[] { client.PlayerData.P1, client.PlayerData.P2, client.PlayerData.P3, client.PlayerData.R1, client.PlayerData.R2, client.PlayerData.R3 }); + vars.Add("MX", client.PlayerData.Mx); + vars.Add("F", client.PlayerData.F); + vars.Add("MBF", client.PlayerData.Mbf); + + // user event + string? ue = spvData.Get("UE"); + if (ue != null) { + long time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if ((time - client.PlayerData.last_ue_time) > 499 || Configuration.ServerConfiguration.AllowChaos) { + vars.Add("UE", ue); + client.PlayerData.last_ue_time = time; + } + } + // pitch + float? cup = spvData.Get("CUP"); + if (cup != null) + vars.Add("CUP", (float)cup); + + vars.Add("NT", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); + vars.Add("t", (int)(Runtime.CurrentRuntime / 1000)); + vars.Add("MID", client.ClientID); + + container.Add(vars); + obj.Add("arr", container); + cmd.Add("c", "SPV"); + cmd.Add("p", obj); + + NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); + client.Room?.Send(packet, client); + } +} diff --git a/src/CommandHandlers/SetUserVariablesHandler.cs b/src/CommandHandlers/SetUserVariablesHandler.cs new file mode 100644 index 0000000..300a556 --- /dev/null +++ b/src/CommandHandlers/SetUserVariablesHandler.cs @@ -0,0 +1,94 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("SUV")] +class SetUserVariablesHandler : CommandHandler { + NetworkObject suvData; + Client client; + string? uid; + public override Task Handle(Client client, NetworkObject receivedObject) { + if (client.Room == null) { + Console.WriteLine($"SUV Missing Room IID: {client.ClientID}"); + client.Send(NetworkObject.WrapObject(0, 1006, new NetworkObject()).Serialize()); + client.ScheduleDisconnect(); + return Task.CompletedTask; + } + this.client = client; + suvData = receivedObject.Get("p"); + uid = suvData.Get("UID"); + + // TODO + if (uid != null && (client.PlayerData.Uid != uid || !client.PlayerData.IsValid)) { + Console.WriteLine($"SUV {client.Room.Name} ({client.Room.ClientsCount}) IID: {client.ClientID} UID: {uid}"); + client.PlayerData.Uid = uid; + client.PlayerData.InitFromNetworkData(suvData); + UpdatePlayersInRoom(); + SendSUVToPlayerInRoom(); + if (client.Room is SpecialRoom room) room.SendAllAlerts(client); + } else { + UpdateVars(); + } + + return Task.CompletedTask; + } + + private void UpdateVars() { + bool updated = false; + NetworkArray vl = new(); + NetworkObject data = new(); + + foreach (string varName in PlayerData.SupportedVariables) { + string? value = suvData.Get(varName); + if (value != null) { + value = client.PlayerData.SetVariable(varName, value); + updated = true; + data.Add(varName, value); + vl.Add(NetworkArray.Param(varName, value)); + } + } + + if (updated) { + client.SendSUV(vl, data); + } + } + + private void UpdatePlayersInRoom() { + NetworkObject data = new(); + NetworkObject acknowledgement = new(); + data.Add("r", client.Room.Id); + + NetworkArray user = client.PlayerData.GetNetworkData(client.ClientID, out NetworkArray playerData); + data.Add("u", user); + + acknowledgement.Add("u", client.ClientID); + acknowledgement.Add("vl", playerData); + NetworkPacket ackPacket = NetworkObject.WrapObject(0, 12, acknowledgement).Serialize(); + NetworkObject obj = ackPacket.GetObject(); + ackPacket.Compress(); + client.Send(ackPacket); + + NetworkPacket packet = NetworkObject.WrapObject(0, 1000, data).Serialize(); + packet.Compress(); + + client.Room.Send(packet, client); + } + + private void SendSUVToPlayerInRoom() { + NetworkObject cmd = new(); + NetworkObject obj = new(); + + cmd.Add("c", "SUV"); + if (client.OldApi) { + obj.Add("MID", client.ClientID.ToString()); + } else { + obj.Add("MID", client.ClientID); + } + cmd.Add("p", obj); + + NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); + client.Room.Send(packet, client); + } +} diff --git a/src/CommandHandlers/UDPSNPCommandHandler.cs b/src/CommandHandlers/UDPSNPCommandHandler.cs new file mode 100644 index 0000000..4a3ed16 --- /dev/null +++ b/src/CommandHandlers/UDPSNPCommandHandler.cs @@ -0,0 +1,22 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text; + +namespace sodoffmmo.CommandHandlers +{ + [ExtensionCommandHandler("SNP")] + class UDPSNPCommandHandler : CommandHandler + { + public override async Task Handle(Client client, NetworkObject receivedObject) + { + // this is only sent over UDP, so we may need to send this over UDP aswell + + var res = Utils.ArrNetworkPacket(["SNP", "1"], ""); + client.Send(res, true); + } + } +} diff --git a/src/CommandHandlers/WorldEventHandlers.cs b/src/CommandHandlers/WorldEventHandlers.cs new file mode 100644 index 0000000..2c6ab12 --- /dev/null +++ b/src/CommandHandlers/WorldEventHandlers.cs @@ -0,0 +1,134 @@ +using System.Globalization; +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("wex.WES")] // event status request +class WorldEventStatusHandler : CommandHandler { + public override Task Handle(Client client, NetworkObject receivedObject) { + client.Send(Utils.ArrNetworkPacket( new string[] { + "WESR", + "WE_" + Configuration.ServerConfiguration.EventName + "|" + WorldEvent.Get().EventInfo(), + "EvEnd|" + WorldEvent.Get().GetLastResults() + })); + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("wex.OV")] +class WorldEventHealthHandler : CommandHandler { + // rec: {"a":13,"c":1,"p":{"c":"wex.OV","p":{"en":"","event":"ScoutAttack","eventUID":"ZydLUmCC","oh":"0.003444444","uid":"ZydLUmCC1"},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + // NOTE: this should be process on event in any state - we use it to make event active + NetworkObject p = receivedObject.Get("p"); + float healthUpdateVal = float.Parse( + p.Get("oh"), + System.Globalization.CultureInfo.InvariantCulture + ); + string targetUid = p.Get("uid"); + + float health = WorldEvent.Get().UpdateHealth(targetUid, healthUpdateVal); + + if (health >= 0.0f) { + // send: {"a":11,"c":0,"p":{"r":367256,"vl":[["WEH_ZydLUmCC1",4,"0.33133352,Thu Jun 22 02:02:43 UTC 2023",false,false]]}} + NetworkPacket packet = Utils.VlNetworkPacket( + "WEH_" + targetUid, + health.ToString("0.0#####", CultureInfo.GetCultureInfo("en-US")) + "," + DateTime.UtcNow.ToString("ddd MMM dd HH:mm:ss UTC yyyy", CultureInfo.GetCultureInfo("en-US")), + WorldEvent.Get().GetRoom().Id + ); + WorldEvent.Get().GetRoom().Send(packet); + } + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("wex.OVF")] // flare info from ship AI -> resend as WEF_ +class WorldEventFlareHandler : CommandHandler { + // rec: {"a":13,"c":1,"p":{"c":"wex.OVF","p":{"en":"","fuid":"WpnpDyJ51,14,0","oh":"0","ts":"6/29/2023 3:03:18 AM"},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + if (!WorldEvent.Get().IsActive()) + return Task.CompletedTask; + + NetworkObject p = receivedObject.Get("p"); + + // send: {"a":11,"c":0,"p":{"r":403777,"vl":[["WEF_WpnpDyJ51,14,0",4,"0,6/29/2023 3:03:18 AM",false,false]]}} + NetworkPacket packet = Utils.VlNetworkPacket( + "WEF_" + p.Get("fuid"), + p.Get("oh") + "," + p.Get("ts"), + WorldEvent.Get().GetRoom().Id + ); + WorldEvent.Get().GetRoom().Send(packet); + + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("wex.ST")] // missile info from ship AI -> resend as WA +class WorldEventMissileHandler : CommandHandler { + // rec: {"a":13,"c":1,"p":{"c":"wex.ST","p":{"en":"","objID":"-4X_gWAo1","tID":"f5b6254a-df78-4e24-aa9d-7e14539fb858","uID":"1f8eeb6b-753f-4e7f-af13-42cdd69d14e7","wID":"5"},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + if (!WorldEvent.Get().IsActive()) + return Task.CompletedTask; + + NetworkObject p = receivedObject.Get("p"); + + // send: {"a":13,"c":1,"p":{"c":"","p":{"arr":["WA","1f8eeb6b-753f-4e7f-af13-42cdd69d14e7","5","f5b6254a-df78-4e24-aa9d-7e14539fb858","-4X_gWAo1"]}}} + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "WA", + p.Get("uID"), + p.Get("wID"), + p.Get("tID"), + p.Get("objID") + }); + WorldEvent.Get().GetRoom().Send(packet); + + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("wex.PS")] +class WorldEventScoreHandler : CommandHandler { + // rec: {"a":13,"c":1,"p":{"c":"wex.PS","p":{"ScoreData":"Datashyo/10","en":"","id":"ScoutAttack"},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + if (!WorldEvent.Get().IsActive()) + return Task.CompletedTask; + + string scoreData = receivedObject.Get("p").Get("ScoreData"); + string[] keyValPair = scoreData.Split('/'); + WorldEvent.Get().UpdateScore(keyValPair[0], keyValPair[1]); + + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("wex.AIACK")] // AI ack +class WorldEventAIACKHandler : CommandHandler { + // rec: {"a":13,"c":1,"p":{"c":"wex.AIACK","p":{"en":"","id":"f322dd98-e9fb-4b2d-a5e0-1c98680517b5","uid":"SoDOff1"},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + WorldEvent.Get().UpdateAI(client); + return Task.CompletedTask; + } +} +[ExtensionCommandHandler("wex.AIP")] // AI ping +class WorldEventAIPingHandler : CommandHandler { + // rec: {"a":13,"c":1,"p":{"c":"wex.AIP","p":{"en":""},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + WorldEvent.Get().UpdateAI(client); + return Task.CompletedTask; + } +} + +[ExtensionCommandHandler("wex.ETS")] // time span +class WorldEventTimeSpanHandler : CommandHandler { + // rec: {"a":13,"c":1,"p":{"c":"wex.ETS","p":{"en":"","timeSpan":"300"},"r":-1}} + public override Task Handle(Client client, NetworkObject receivedObject) { + float timeSpan = float.Parse( + receivedObject.Get("p").Get("timeSpan"), + System.Globalization.CultureInfo.InvariantCulture + ); + WorldEvent.Get().SetTimeSpan(client, timeSpan); + return Task.CompletedTask; + } +} diff --git a/src/Core/ApiWebService.cs b/src/Core/ApiWebService.cs new file mode 100644 index 0000000..f4af081 --- /dev/null +++ b/src/Core/ApiWebService.cs @@ -0,0 +1,126 @@ +using System; +using System.Net.Http.Json; +using sodoffmmo.CommandHandlers; + +namespace sodoffmmo.Core; + +public class ApiWebService +{ + public UserBanType? CheckForUserBan(Client client) + { + HttpClient httpClient = new(); + var content = new FormUrlEncodedContent( + new Dictionary { + { "token", client.PlayerData.UNToken } + } + ); + httpClient.Timeout = new TimeSpan(0, 0, 3); + + try + { + var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Moderation/CheckForVikingBan", content).Result; + Log("Moderation/CheckForVikingBan"); + if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadFromJsonAsync().Result; + else return null; + } catch (Exception e) { LogError(e.Message); return null; } + } + + public string? BanUser(Client client, string userId, string banType, string days) + { + HttpClient httpClient = new(); + var content = new FormUrlEncodedContent( + new Dictionary { + { "token", client.PlayerData.UNToken }, + { "userId", userId }, + { "banType", banType }, + { "days", days } + } + ); + httpClient.Timeout = new TimeSpan(0, 0, 3); + + try + { + var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Moderation/AddBanToVikingByGuid", content).Result; + Log("Moderation/AddBanToVikingByGuid"); + + if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadAsStringAsync().Result; + else return null; + } catch (Exception e) { LogError(e.Message); return null; } + } + + public bool SendMessageBoard(Client client, string userId, string data, string level, string replyMessageId) + { + HttpClient httpClient = new(); + var content = new FormUrlEncodedContent + ( + new Dictionary + { + { "token", client.PlayerData.UNToken }, + { "userId", userId }, + { "data", data }, + { "messageLevel", level }, + { "replyMessageId", replyMessageId } + } + ); + httpClient.Timeout = new TimeSpan(0, 0, 3); + + try + { + var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Messaging/PostTextMessage", content).Result; + Log("Messaging/PostTextMessage"); + + if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadFromJsonAsync().Result; + else return false; + } catch (Exception e) { LogError(e.Message); return false; } + } + + public bool SetOnline(Client client, bool online) + { + HttpClient httpClient = new(); + var content = new FormUrlEncodedContent + ( + new Dictionary + { + { "token", client.PlayerData.UNToken }, + { "online", online.ToString() } + } + ); + + try + { + var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Precense/SetVikingOnline", content).Result; + Log("Precense/SetVikingOnline"); + + if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadFromJsonAsync().Result; + else return false; + } catch (Exception e) { LogError(e.Message); return false; } + } + + public bool SetRoom(Client client, int roomId, string zoneName) + { + HttpClient httpClient = new(); + var content = new FormUrlEncodedContent + ( + new Dictionary + { + { "token", client.PlayerData.UNToken }, + { "roomId", roomId.ToString() }, + { "zoneName", zoneName } + } + ); + + try + { + var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Precense/SetVikingRoom", content).Result; + Log("Precense/SetVikingRoom"); + + if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadFromJsonAsync().Result; + else return false; + } + catch (Exception e) { LogError(e.Message); return false; } + } + + private void Log(string endpoint) => Console.WriteLine($"Sent API Request To {Configuration.ServerConfiguration.ApiUrl}/{endpoint}"); + + private void LogError(string message) => Console.WriteLine($"An Error Has Occured When Sending An API Request - {message}"); +} diff --git a/src/Core/Client.cs b/src/Core/Client.cs new file mode 100644 index 0000000..073d172 --- /dev/null +++ b/src/Core/Client.cs @@ -0,0 +1,164 @@ +using sodoffmmo.Data; +using System; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; + +namespace sodoffmmo.Core; +public class Client { + static int id; + static object lck = new(); + + public int ClientID { get; private set; } + public IPEndPoint? UDPEndPoint { get; set; } + public PlayerData PlayerData { get; set; } = new(); + public Room? Room { get; private set; } + public bool OldApi { get; set; } = false; + public bool TempMuted { get; set; } = false; + + private readonly Socket socket; + SocketBuffer socketBuffer = new(); + private volatile bool scheduledDisconnect = false; + private readonly object clientLock = new(); + + public Client(Socket clientSocket) { + socket = clientSocket; + lock (lck) { + ClientID = ++id; + } + } + + public async Task Receive() { + byte[] buffer = new byte[2048]; + int len = await socket.ReceiveAsync(buffer, SocketFlags.None); + if (len == 0) + throw new SocketException(); + socketBuffer.Write(buffer, len); + } + + public bool TryGetNextPacket(out NetworkPacket packet) { + return socketBuffer.ReadPacket(out packet); + } + + public void Send(NetworkPacket packet, bool useUDP = false) { + try { + if (useUDP && UDPEndPoint != null) + { + Server.SharedUDPClient.Send(packet.SendData, packet.SendData.Length, UDPEndPoint); + return; + } + socket.Send(packet.SendData); + } catch (Exception ex) { + Console.WriteLine(ex.ToString()); + ScheduleDisconnect(); + } + } + + public void SetRoom(Room? room) { + // api web service for setting precense + ApiWebService apiWebService = new(); + + lock (clientLock) { + // set variable player data as not valid, but do not reset all player data + PlayerData.IsValid = false; + + if (Room != null) { + Console.WriteLine($"Leave room: {Room.Name} (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}"); + Room.RemoveClient(this); + + NetworkObject data = new(); + data.Add("r", Room.Id); + data.Add("u", ClientID); + Room.Send(NetworkObject.WrapObject(0, 1004, data).Serialize()); + + apiWebService.SetOnline(this, false); + apiWebService.SetRoom(this, 0, string.Empty); + } + + // set new room (null when SetRoom is used as LeaveRoom) + Room = room; + + if (Room != null) { + Console.WriteLine($"Join room: {Room.Name} RoomID (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}"); + Room.AddClient(this); + + Send(Room.SubscribeRoom()); + if (Room.Name != "LIMBO") UpdatePlayerUserVariables(); // do not update user vars if room is limbo + + apiWebService.SetOnline(this, true); + + if (Room.Name != "LIMBO") + { + UpdatePlayerUserVariables(); + apiWebService.SetRoom(this, Room.Id, Room.Name); + } // do not update user vars or set room if limbo + } + } + } + + private void UpdatePlayerUserVariables() { + foreach (Client c in Room.Clients) { + NetworkObject cmd = new(); + NetworkObject obj = new(); + cmd.Add("c", "SUV"); + if (OldApi) { + obj.Add("MID", c.ClientID.ToString()); + } else { + obj.Add("MID", c.ClientID); + } + cmd.Add("p", obj); + Send(NetworkObject.WrapObject(1, 13, cmd).Serialize()); + } + } + + public void SendSUV(NetworkArray vl, NetworkObject data) { + NetworkObject data2 = new(); + data2.Add("u", ClientID); + data2.Add("vl", vl); + NetworkPacket packet = NetworkObject.WrapObject(0, 12, data2).Serialize(); + Room.Send(packet); + + NetworkObject cmd = new(); + cmd.Add("c", "SUV"); + NetworkArray arr = new(); + if (OldApi) { + data.Add("MID", ClientID.ToString()); + } else { + data.Add("MID", ClientID); + } + data.Add("RID", Room.Id.ToString()); + arr.Add(data); + NetworkObject container = new(); + container.Add("arr", arr); + cmd.Add("p", container); + packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); + Room.Send(packet, this); + } + + public void Disconnect() { + try { + socket.Shutdown(SocketShutdown.Both); + } finally { + socket.Close(); + } + } + + public void ScheduleDisconnect() { + ApiWebService apiWebService = new(); + if (Room != null) + { + // quiet remove from room (to avoid issues in Room.Send) + // - do not change Room value here + // - full remove will be will take place Server.HandleClient (before real disconnected) + Room.RemoveClient(this); + apiWebService.SetOnline(this, false); + } + scheduledDisconnect = true; + } + + public bool Connected { + get { + return socket.Connected && !scheduledDisconnect; + } + } +} diff --git a/src/Core/CommandHandler.cs b/src/Core/CommandHandler.cs new file mode 100644 index 0000000..5d6b7a1 --- /dev/null +++ b/src/Core/CommandHandler.cs @@ -0,0 +1,8 @@ +using sodoffmmo.Data; + +namespace sodoffmmo.Core; +public abstract class CommandHandler { + public bool RunInBackground { get; } + + public abstract Task Handle(Client client, NetworkObject receivedObject); +} diff --git a/src/Core/Configuration.cs b/src/Core/Configuration.cs new file mode 100644 index 0000000..d7a2d69 --- /dev/null +++ b/src/Core/Configuration.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; + +namespace sodoffmmo.Core; +internal static class Configuration { + + public static ServerConfiguration ServerConfiguration { get; private set; } = new ServerConfiguration(); + + public static void Initialize() { + IConfigurationRoot config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", true) + .Build(); + + ServerConfiguration? serverConfiguration = config.GetSection("MMOServer").Get(); + if (serverConfiguration is null) + return; + + ServerConfiguration = serverConfiguration; + if (string.IsNullOrEmpty(ServerConfiguration.ApiUrl)) { + ServerConfiguration.Authentication = AuthenticationMode.Disabled; + } + } +} + +internal sealed class ServerConfiguration { + public string ListenIP { get; set; } = string.Empty; + public int Port { get; set; } = 9933; + public string EventName { get; set; } = "ScoutAttack"; + public int EventTimer { get; set; } = 30; + public int FirstEventTimer { get; set; } = 10; + public Dictionary RoomAlerts { get; set; } = new(); + public string[] AmbassadorRooms { get; set; } = Array.Empty(); + public int AmbassadorGaugeStart { get; set; } = 75; + public float AmbassadorGaugeDecayRate { get; set; } = 60; + public bool AmbassadorGaugeDecayOnlyWhenInRoom { get; set; } = true; + public float AmbassadorGaugePlayers { get; set; } = 0.5f; + public int RacingMaxPlayers { get; set; } = 6; + public int RacingMinPlayers { get; set; } = 2; + public int RacingMainLobbyTimer { get; set; } = 15; + public int PingDelay { get; set; } = 17; + public bool EnableChat { get; set; } = true; + public bool EnableCannedChat { get; set; } = true; + public bool AllowChaos { get; set; } = false; + public AuthenticationMode Authentication { get; set; } = AuthenticationMode.Disabled; + public string ApiUrl { get; set; } = ""; + public string BypassToken { get; set; } = ""; +} + +public enum AuthenticationMode { + Disabled, Optional, RequiredForChat, Required +} diff --git a/src/Core/GauntletRoom.cs b/src/Core/GauntletRoom.cs new file mode 100644 index 0000000..1f6fb4d --- /dev/null +++ b/src/Core/GauntletRoom.cs @@ -0,0 +1,130 @@ +using System; +using sodoffmmo.Data; + +namespace sodoffmmo.Core; +public class GauntletRoom : Room { + static object NextRoomLock = new object(); + static GauntletRoom? NextRoom = null; + + public static GauntletRoom Get() { + lock(NextRoomLock) { + if (NextRoom != null && NextRoom.ClientsCount == 1) { + var ret = NextRoom!; + NextRoom = null; + return ret; + } else { + NextRoom = new GauntletRoom(); + return NextRoom!; + } + } + } + + public GauntletRoom() : base (null, "GauntletDO", true) { + base.RoomVariables.Add(NetworkArray.VlElement("IS_RACE_ROOM", true)); + } + + class Status { + public string uid; + public bool isReady = false; + public string resultA = ""; + public string resultB = ""; + + public Status(string uid) { + this.uid = uid; + } + } + + private Dictionary players = new(); + + public void AddPlayer(Client client) { + players[client] = new Status(client.PlayerData.Uid); + } + + public void SetPlayerReady(Client client, bool status = true) { + players[client].isReady = status; + } + + public int GetReadyCount() { + int count = 0; + foreach(var player in players) { + if (player.Value.isReady) ++count; + } + return count; + } + + public void SendUJR() { + // {"a":13,"c":1,"p":{"c":"msg","p":{"arr":["UJR","287997","2","f66cc516-7ea3-40a5-9021-01ff8f290123","false","2","03a3ad99-87a5-4af4-8966-0b2733a05e0f","false","1"]},"r":287997}} + List info = new(); + info.Add("UJR"); // User Joined Room + info.Add(base.Id.ToString()); + info.Add("2"); + foreach(var player in players) { + info.Add(player.Value.uid); + info.Add(player.Value.isReady.ToString()); + info.Add("1"); // TODO this should be player gender + } + NetworkPacket packet = Utils.ArrNetworkPacket(info.ToArray(), "msg", base.Id); + + Send(packet); + } + + public void SendPA(Client client) { + // {"a":13,"c":1,"p":{"c":"msg","p":{"arr":["UJR","287997","2","f66cc516-7ea3-40a5-9021-01ff8f290123","false","2","03a3ad99-87a5-4af4-8966-0b2733a05e0f","false","1"]},"r":287997}} + List info = new(); + info.Add("PA"); // Play Again + info.Add(base.Id.ToString()); + info.Add("1"); + foreach(var player in players) { + info.Add(player.Value.uid); + info.Add(player.Value.isReady.ToString()); + info.Add("1"); // TODO this should be player gender + } + NetworkPacket packet = Utils.ArrNetworkPacket(info.ToArray(), "msg", base.Id); + + client.Send(packet); + } + + public bool ProcessResult(Client client, string resultA, string resultB) { + lock (base.roomLock) { + players[client].resultA = resultA; + players[client].resultB = resultB; + + int count = 0; + foreach(var player in players) { + if (player.Value.resultB != "") ++count; + } + if (count != 2) + return false; + + // {"a":13,"c":1,"p":{"c":"msg","p":{"arr":["GC","365587","03a3ad99-87a5-4af4-8966-0b2733a05e0f","10850","79","1","bff0c312-8763-497d-aa0c-a5dfc7d8b861","21050","73","1"]},"r":365587}} + List info = new(); + info.Add("GC"); + info.Add(base.Id.ToString()); + foreach(var player in players) { + if (player.Value.resultB == "") + continue; + info.Add(player.Value.uid); + info.Add(player.Value.resultA); + info.Add(player.Value.resultB); + info.Add("1"); + } + NetworkPacket packet = Utils.ArrNetworkPacket(info.ToArray(), "msg", base.Id); + + Send(packet); + return true; + } + } + + static object joinLock = new object(); + + static public void Join(Client client, GauntletRoom? room = null) { + lock(joinLock) { + if (room is null) + room = GauntletRoom.Get(); + + client.SetRoom(room); + room.AddPlayer(client); // client will be not removed from GauntletRoom.players ... after remove all client from room whole GauntletRoom.players will be removed + room.SendUJR(); + } + } +} diff --git a/src/Core/ModuleManager.cs b/src/Core/ModuleManager.cs new file mode 100644 index 0000000..a4c31ad --- /dev/null +++ b/src/Core/ModuleManager.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using sodoffmmo.Attributes; + +namespace sodoffmmo.Core; + +class ModuleManager { + Dictionary handlers = new(); + Dictionary extHandlers = new(); + + public void RegisterModules() { + RegisterHandlers(); + RegisterExtensionHandlers(); + } + + public CommandHandler GetCommandHandler(int id) { + if (handlers.TryGetValue(id, out Type? handler)) + return (CommandHandler)Activator.CreateInstance(handler)!; + throw new Exception($"Command handler with ID {id} not found!"); + } + + public CommandHandler GetCommandHandler(string name) { + if (extHandlers.TryGetValue(name, out Type? handler)) + return (CommandHandler)Activator.CreateInstance(handler)!; + throw new Exception($"Command handler with name \"{name}\" not found!"); + } + + private void RegisterHandlers() { + handlers.Clear(); + var handlerTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(type => typeof(CommandHandler).IsAssignableFrom(type)) + .Where(type => type.GetCustomAttribute() != null); + + foreach (var handlerType in handlerTypes) { + CommandHandlerAttribute attrib = (handlerType.GetCustomAttribute(typeof(CommandHandlerAttribute)) as CommandHandlerAttribute)!; + handlers[attrib.ID] = handlerType; + } + } + + private void RegisterExtensionHandlers() { + extHandlers.Clear(); + var extHandlerTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(type => typeof(CommandHandler).IsAssignableFrom(type)) + .Where(type => type.GetCustomAttribute() != null); + + foreach (var extHandlerType in extHandlerTypes) { + ExtensionCommandHandlerAttribute attrib = (extHandlerType.GetCustomAttribute(typeof(ExtensionCommandHandlerAttribute)) as ExtensionCommandHandlerAttribute)!; + extHandlers[attrib.Name] = extHandlerType; + } + } +} \ No newline at end of file diff --git a/src/Core/Racing.cs b/src/Core/Racing.cs new file mode 100644 index 0000000..843d784 --- /dev/null +++ b/src/Core/Racing.cs @@ -0,0 +1,328 @@ +using System.Globalization; +using sodoffmmo.Data; +using System.Timers; + +namespace sodoffmmo.Core; + +public enum RacingPlayerState { + NotReady, + Ready, + InRacingRoom, + RaceReady1, + RaceReady2 +} + +public class RacingRoom : Room { + static Random random = new Random(); + + public static RacingRoom Get() { + return new RacingRoom(); + } + + public RacingRoom() : base (null, "RacingDragon", true) { + TID = random.Next(105); + + players = new(); + results = new(); + timer = null; + + base.RoomVariables = new(); + base.RoomVariables.Add(NetworkArray.VlElement("IS_RACE_ROOM", "SINGLERACE#1#1#SINGLERACE#0#2")); + base.RoomVariables.Add(NetworkArray.VlElement("TID", TID)); + } + + public int TID; + + // players ready status + + Dictionary players; + + public void SetPlayerState(Client client, RacingPlayerState state) { + players[client] = state; + } + + public bool IsPlayerState(Client client, RacingPlayerState state) { + if (players.TryGetValue(client, out var info)) { + return info == state; + } + return false; + } + + public int GetPlayersCount(RacingPlayerState state) { + int count = 0; + foreach(var player in players) { + if (player.Value == state) ++count; + } + return count; + } + + // results + + class Result { + public string userName; + public string time; + public string laps; + + public Result(string userName, string time, string laps) { + this.userName = userName; + this.time = time; + this.laps = laps; + } + } + + SortedDictionary results; + + public void SetResults(Client client, string userName, string time, string laps) { + float timef = float.Parse(time, System.Globalization.CultureInfo.InvariantCulture); + while (results.ContainsKey(timef)) + timef += 0.000001f; + + results.Add(timef, new Result(userName, time, laps)); + } + + public void SendResults() { + if (ClientsCount == results.Count) { + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","GR","Zavertin","91.81613","3","scourgexxwulf","111.81613","3"]},"r":412467}} + List info = new(); + info.Add("RA"); + info.Add(""); + info.Add("GR"); + foreach(var result in results) { + info.Add(result.Value.userName); + info.Add(result.Value.time); + info.Add(result.Value.laps); + } + + NetworkPacket packet = Utils.ArrNetworkPacket(info.ToArray(), "", Id); + Send(packet); + } + } + + // start and countdown + + System.Timers.Timer? timer = null; + int counter; + + private void SetTimer(double timeout, System.Timers.ElapsedEventHandler callback, bool AutoReset = false) { + if (timer != null) { + timer.Stop(); + timer.Close(); + } + + timer = new System.Timers.Timer(timeout * 1000); + timer.AutoReset = AutoReset; + timer.Enabled = true; + timer.Elapsed += callback; + } + + public void Init() { + counter = 20; + SetTimer(0.2, SendJoin); + } + + private void SendJoin(Object? source, ElapsedEventArgs e) { + foreach(var player in players) { + player.Key.SetRoom(this); + } + SetTimer(1, CountDown, true); + } + + private void CountDown(Object? source, ElapsedEventArgs e) { + if (!TryLoad()) { + if (counter == 0) { + Load(); + } else { + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","LT","18"]},"r":412467}} + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "LT", + (--counter).ToString() + }, "", Id); + Send(packet); + } + } + } + + public bool TryLoad() { + if (GetPlayersCount(RacingPlayerState.Ready) == ClientsCount) { + Load(); + return true; + } + return false; + } + + public void Load() { + timer!.Stop(); + timer!.Close(); + + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","ST"]},"r":412467}} + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "ST" + }, "", Id); + Send(packet); + } + + // TODO StratTimer → kick out to main lobby players without RacingPlayerState.RaceReady1 after timeout, next kick out players without RacingPlayerState.RaceReady2 after timeout2 + + // TODO EndTimer → {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","ET","1"]},"r":412467}} + + // utils + + public NetworkPacket GetTIDPacket() { + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","TID","","10","0","0","0","0"]}}} + return Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "TID", + "", // game mode (unused) + TID.ToString(), // track id + "0", // theme id + "0","0","0" // unused (?) + }); + } + + public NetworkPacket GetSTAPacket() { + string staList = ""; + int i = 0; + foreach(var player in players) { + if (i > 0) + staList += ":"; + staList += player.Key.PlayerData.Uid + ":" + (++i).ToString(); + } + + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","STA","c4647597-a72a-4f34-973c-5a10218d9a64:1:f05fc387-7358-4bff-be04-7c316f0a8de8:2:ae70ef16-c52c-43e4-9305-d6ea3e378a0d:3","1688128174649"]},"r":412467}} + return Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "STA", + staList + }, "", Id); + } +} + +public class RacingLobby { + static object lobbyLock = new object(); + public readonly static RacingLobby Lobby = new RacingLobby(); + + private RacingLobby() { + timer = new System.Timers.Timer(1000); + timer.AutoReset = true; + timer.Enabled = true; + timer.Elapsed += CheckRacingRoomCountdown; + } + + System.Timers.Timer timer; + int counter; + + private void CheckRacingRoomCountdown(Object? source, System.Timers.ElapsedEventArgs e) { + lock (lobbyLock) { + int readyPlayersCount = GetPlayersCount(RacingPlayerState.Ready); + if (readyPlayersCount >= Configuration.ServerConfiguration.RacingMaxPlayers) { + SendToRacingRoom(); + counter = -1; + } else if (readyPlayersCount >= Configuration.ServerConfiguration.RacingMinPlayers) { + if (counter < 0) { + counter = Configuration.ServerConfiguration.RacingMainLobbyTimer; + } + if (--counter == 0) { + SendToRacingRoom(); + counter = -1; + } + } else { + counter = -1; + } + } + } + + class Status { + public string uid; + public RacingPlayerState state = RacingPlayerState.NotReady; + public Status (string uid) { + this.uid = uid; + } + } + + Dictionary lobbyPlayers = new(); + + public void SetPlayerState(Client client, RacingPlayerState state) { + lock (lobbyLock) { + if (!lobbyPlayers.ContainsKey(client)) { + lobbyPlayers[client] = new Status(client.PlayerData.Uid); + } + lobbyPlayers[client].state = state; + } + } + + public bool IsPlayerState(Client client, RacingPlayerState state) { + lock (lobbyLock) { + if (lobbyPlayers.TryGetValue(client, out var info)) { + return info.state == state; + } + return false; + } + } + + public int GetPlayersCount(RacingPlayerState state) { + lock (lobbyLock) { + int count = 0; + foreach(var player in lobbyPlayers) { + if (player.Value.state == state) ++count; + } + return count; + } + } + + private bool SendToRacingRoom() { + // lock (lobbyLock) { + if (GetPlayersCount(RacingPlayerState.Ready) >= Configuration.ServerConfiguration.RacingMinPlayers) { + int i = 0; + RacingRoom room = RacingRoom.Get(); + foreach (var player in lobbyPlayers) { + if (player.Value.state == RacingPlayerState.Ready) { + if (++i > Configuration.ServerConfiguration.RacingMaxPlayers) + break; + // set client state in Lobby + player.Value.state = RacingPlayerState.InRacingRoom; + // send TID info to client + player.Key.Send(room.GetTIDPacket()); + // set client state in racing room + room.SetPlayerState(player.Key, RacingPlayerState.NotReady); + } + } + // join clients to racing room and start countdown + // after change room, client will be removed from lobbyPlayers (in GetPS) + room.Init(); + + return true; + } + return false; + // } + } + + public NetworkPacket GetPS() { + List toRemove = new(); + Room room = Room.Get("DragonRacingDO"); + + // {"a":13,"c":1,"p":{"c":"PS","p":{"arr":["RA","","PS","e6147216-8100-4552-864d-be8f1347e201","8cb5842d-735a-4259-80af-e2e204b9c2bd"]}}} + List info = new(); + info.Add("RA"); + info.Add(""); + info.Add("PS"); + foreach(var player in lobbyPlayers) { + if (player.Key.Room != room) { + toRemove.Add(player.Key); + } else if (player.Value.state != RacingPlayerState.InRacingRoom) { + info.Add(player.Value.uid); + } + } + + foreach (var player in toRemove) { + lobbyPlayers.Remove(player); + } + + return Utils.ArrNetworkPacket(info.ToArray(), "PS"); + } +} diff --git a/src/Core/Room.cs b/src/Core/Room.cs new file mode 100644 index 0000000..3f41ac9 --- /dev/null +++ b/src/Core/Room.cs @@ -0,0 +1,160 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Net.Sockets; +using sodoffmmo.Data; + +namespace sodoffmmo.Core; +public class Room { + public static int MaxId { get; private set; } = 2; + static object RoomsListLock = new object(); + protected static Dictionary rooms = new(); + + List clients = new(); + protected object roomLock = new object(); + + public int Id { get; private set; } + public string Name { get; private set; } + public string Group { get; private set; } + public bool AutoRemove { get; private set; } + public bool IsRemoved { get; private set; } = false; + + public bool AllowChatOverride { get; set; } = false; + public NetworkArray RoomVariables = new(); + + public Room(string? name, string? group = null, bool autoRemove = false) { + Id = ++MaxId; + if (name is null) { + Name = group! + "_" + MaxId; + } else { + Name = name; + } + if (group is null) { + Group = name!; + } else { + Group = group; + } + AutoRemove = autoRemove; + rooms.Add(Name, this); + } + + public int ClientsCount => clients.Count; + + public IEnumerable Clients { + get { + List list; + lock (roomLock) { + list = new List(clients); + } + return list; + } + } + + public void AddClient(Client client) { + lock (roomLock) { + if (IsRemoved) + throw new Exception("Call AddClient on removed room"); + client.Send(RespondJoinRoom()); + // NOTE: send RespondJoinRoom() and add client to clients as atomic operation + // to make sure to client get full list of players in room + clients.Add(client); + } + } + + public void RemoveClient(Client client) { + lock (roomLock) { + clients.Remove(client); + if (AutoRemove && ClientsCount == 0) { + IsRemoved = true; + rooms.Remove(Name); + } + } + } + + public void Send(NetworkPacket packet, Client? skip = null, bool useUDP = false) { + foreach (var roomClient in Clients) { + if (roomClient != skip) { + roomClient.Send(packet, useUDP); + } + } + } + + public static bool Exists(string name) => rooms.ContainsKey(name); + + public static Room Get(string name) => rooms[name]; + + public static Room GetOrAdd(string name, bool autoRemove = false) { + lock(RoomsListLock) { + if (!Room.Exists(name)) + return new Room(name, autoRemove: autoRemove); + return rooms[name]; + } + } + + public static Room[] AllRooms() { + return rooms.Values.ToArray(); + } + + public static void DisableAllChatOverrides() { + lock (RoomsListLock) { + foreach (var room in rooms) { + room.Value.AllowChatOverride = false; + } + } + } + + public NetworkPacket RespondJoinRoom() { + NetworkObject obj = new(); + NetworkArray roomInfo = new(); + roomInfo.Add(Id); + roomInfo.Add(Name); // Room Name + roomInfo.Add(Group); // Group Name + roomInfo.Add(true); // is game + roomInfo.Add(false); // is hidden + roomInfo.Add(false); // is password protected + roomInfo.Add((short)clients.Count); // player count + roomInfo.Add((short)4096); // max player count + roomInfo.Add(GetRoomVars()); // variables (plus added data) + roomInfo.Add((short)0); // spectator count + roomInfo.Add((short)0); // max spectator count + + NetworkArray userList = new(); + foreach (Client player in clients) { + if (player.PlayerData.Uid != "") + userList.Add(player.PlayerData.GetNetworkData(player.ClientID, out _)); + } + + obj.Add("r", roomInfo); + obj.Add("ul", userList); + + NetworkPacket packet = NetworkObject.WrapObject(0, 4, obj).Serialize(); + packet.Compress(); + + return packet; + } + + public NetworkPacket SubscribeRoom() { + NetworkObject obj = new(); + NetworkArray list = new(); + + NetworkArray r1 = new(); + r1.Add(Id); + r1.Add(Name); // Room Name + r1.Add(Group); // Group Name + r1.Add(true); + r1.Add(false); + r1.Add(false); + r1.Add((short)clients.Count); // player count + r1.Add((short)4096); // max player count + r1.Add(GetRoomVars()); + r1.Add((short)0); + r1.Add((short)0); + + list.Add(r1); + + obj.Add("rl", list); + obj.Add("g", Group); + return NetworkObject.WrapObject(0, 15, obj).Serialize(); + } + + internal virtual NetworkArray GetRoomVars() { return RoomVariables; } +} diff --git a/src/Core/Runtime.cs b/src/Core/Runtime.cs new file mode 100644 index 0000000..0482a84 --- /dev/null +++ b/src/Core/Runtime.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; + +namespace sodoffmmo.Core; +public static class Runtime { + static Runtime() { + var currentProcess = Process.GetCurrentProcess(); + lastSystemTime = (long)(DateTime.Now - currentProcess.StartTime).TotalMilliseconds; + currentProcess.Dispose(); + stopwatch = new Stopwatch(); stopwatch.Start(); + } + + private static long lastSystemTime; + private static Stopwatch stopwatch; + + public static long CurrentRuntime { + get { + return stopwatch.ElapsedMilliseconds + lastSystemTime; + } + } +} diff --git a/src/Core/SWRacing.cs b/src/Core/SWRacing.cs new file mode 100644 index 0000000..ecd44a9 --- /dev/null +++ b/src/Core/SWRacing.cs @@ -0,0 +1,261 @@ +using sodoffmmo.Data; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace sodoffmmo.Core +{ + public class SWRacingRoom : Room + { + + class SWPlayerState + { + public int Index; + public Vector3 PosTransform = new(); + public string Token = ""; + public bool IsHost; + public bool IsReady; + public bool SceneLoaded; + public bool CountdownFinished; + public int ColorIndex; + public string VehicleResName = ""; + public int CurrentLap; + public float FinishedTime = -1f; + public bool IsTrackPrebuilt; + public int TrackIdx; + public string TrackStatus = "ZR"; + } + + public SWRacingRoom() : base(null, "ShipWreckMPRoom", true) + { + } + + public bool CountdownStarted { get; set; } = false; + public bool RaceOngoing { get; set; } = false; + public bool BuddiesOnly { get; set; } = false; + + Dictionary Players = new(); + Client? host = null; + int nextIndex = 1; + + public void AddPlayer(Client client) + { + lock (roomLock) + { + var state = new SWPlayerState { Index = nextIndex++ }; + if(host is null) + { + host = client; + state.IsHost = true; + } + Players[client] = state; + } + } + + public int GetIndex(Client client) + { + lock (roomLock) + { + var state = Players[client]; + return state.Index; + } + } + + public int GetReadyCount() + { + lock (roomLock) + { + int readyCount = 0; + foreach(var (client, s) in Players) + { + if (s.IsReady) + readyCount++; + } + return readyCount; + } + } + + public IEnumerable GetPlayersWithinRadius(Vector3 blast, float hitRadius, Client? excluding) + { + lock (roomLock) + { + float targetRadiusSquared = hitRadius * hitRadius; + List targetedPlayers = []; + + foreach(var player in Players) + { + if (player.Key.ClientID == excluding?.ClientID) continue; + if (Vector3.DistanceSquared(player.Value.PosTransform, blast) < targetRadiusSquared) + targetedPlayers.Add(player.Value.Index); + } + + return targetedPlayers; + } + } + + public void SetPlayerPosition(Client client, float X, float Y, float Z) + { + lock (roomLock) + { + var state = Players[client]; + state.PosTransform.X = X; + state.PosTransform.Y = Y; + state.PosTransform.Z = Z; + } + } + + public void SetReady(Client client, bool ready) + { + lock (roomLock) + { + var state = Players[client]; + state.IsReady = ready; + } + } + + public void SetTrack(Client client, int trackId) + { + lock (roomLock) + { + var state = Players[client]; + state.TrackIdx = trackId; + } + } + + public void SetTrackStatus(Client client, string statusCode) + { + lock (roomLock) + { + var state = Players[client]; + state.TrackStatus = statusCode; + } + } + + public void SetIsPrebuiltTrack(Client client, bool isPrebuiltTrack) + { + lock (roomLock) + { + var state = Players[client]; + state.IsTrackPrebuilt = isPrebuiltTrack; + } + } + + public void SetBoat(Client client, string boatRes, int colorIndex = 0) + { + lock (roomLock) + { + var state = Players[client]; + state.VehicleResName = boatRes; + state.ColorIndex = colorIndex; + } + } + + public void SetToken(Client client, string token) + { + lock (roomLock) + { + var state = Players[client]; + state.Token = token; + } + } + + public bool SetSceneLoaded(Client client) + { + lock (roomLock) + { + Players[client].SceneLoaded = true; + foreach (var (_, s) in Players) + if (!s.SceneLoaded) return false; + return true; + } + } + + public bool SetCountdownFinished(Client client) + { + lock (roomLock) + { + Players[client].CountdownFinished = true; + foreach (var (_, s) in Players) + if (!s.CountdownFinished) return false; + return true; + } + } + + public void SetPlayerLap(Client client, int lap) + { + lock (roomLock) + { + var state = Players[client]; + state.CurrentLap = lap; + } + } + + public void SetFinishedTime(Client client, float time) + { + lock (roomLock) + { + var state = Players[client]; + state.FinishedTime = time; + } + } + + public NetworkPacket GetAMPacket() + { + List info = [ "AM" ]; + foreach(var (client, s) in Players) + { + info.Add(s.IsHost ? "ZJ" : ""); + info.Add(client.PlayerData.UNToken); + info.Add(s.Index.ToString()); + info.Add(s.ColorIndex.ToString()); + info.Add(s.VehicleResName); + info.Add(s.IsReady ? "AN" : ""); + info.Add(s.IsTrackPrebuilt ? "ZP" : "ZQ"); + info.Add(s.TrackIdx.ToString()); + info.Add(s.TrackStatus); + } + + string joined = string.Join("|", info); + + return Utils.ArrNetworkPacket(["AM", joined], "", Id); + } + + System.Timers.Timer? countdownTimer; + int lobbyCounter; + + public void TryStartLobbyCountdown() + { + Console.WriteLine($"Attempted To Start SWL Lobby Countdown On SWL {Id}. Player Count - {Players.Count}"); + + lock (roomLock) + { + if (Players.Count <= 1) return; // this is multiplayers, don't just start a singleplayer time trial lmao + if (CountdownStarted || GetReadyCount() < Players.Count) return; + + string joined = string.Join("|", "AB", Players.Count.ToString(), "3"); + Send(Utils.ArrNetworkPacket([joined], "", Id)); + + CountdownStarted = true; + Console.WriteLine($"Countdown Started On SWL {Id}"); + } + + lobbyCounter = 3; + countdownTimer = new(1000) { AutoReset = true }; + countdownTimer.Elapsed += TickLobbyCountdown; + countdownTimer.Enabled = true; + } + + private void TickLobbyCountdown(object? sender, System.Timers.ElapsedEventArgs e) + { + Console.WriteLine($"Lobby Countdown Tick TS - {DateTime.Now}"); + if(--lobbyCounter <= 0) + { + countdownTimer!.Stop(); + string joined = string.Join("|", "ZH", lobbyCounter.ToString()); + Send(Utils.ArrNetworkPacket([joined], "", Id)); + RaceOngoing = true; + } + } + } +} \ No newline at end of file diff --git a/src/Core/SpecialRoom.cs b/src/Core/SpecialRoom.cs new file mode 100644 index 0000000..e61e4fe --- /dev/null +++ b/src/Core/SpecialRoom.cs @@ -0,0 +1,191 @@ +using sodoffmmo.Data; + +namespace sodoffmmo.Core; + +public class SpecialRoom : Room { + public double[] ambassadorGauges = new double[3]; // There is always a maximum of 3. + System.Timers.Timer? ambassadorTimer; + + public static void CreateRooms() { + foreach (var room in Configuration.ServerConfiguration.RoomAlerts) { + foreach (var alert in room.Value) { + AlertInfo alertInfo = new AlertInfo( + alert[0], // type + float.Parse(alert[1], System.Globalization.CultureInfo.InvariantCulture.NumberFormat), // duration + Int32.Parse(alert[2]), Int32.Parse(alert[3]), // start min - max for random start time + Int32.Parse(alert[4]), Int32.Parse(alert[5]) // extra parameters for specific alarm types + ); + Console.WriteLine($"Setup alert {alertInfo} for {room.Key}"); + (rooms.GetValueOrDefault(room.Key) as SpecialRoom ?? new SpecialRoom(room.Key)).AddAlert(alertInfo); + } + } + + foreach (var room in Configuration.ServerConfiguration.AmbassadorRooms) { + Console.WriteLine($"Setup Ambassador for {room}"); + (rooms.GetValueOrDefault(room) as SpecialRoom ?? new SpecialRoom(room)).InitAmbassador(); + } + } + + public SpecialRoom(string name) : base(name) {} + + public void InitAmbassador() { + for (int i=0;i<3;i++) ambassadorGauges[i] = Configuration.ServerConfiguration.AmbassadorGaugeStart; + ambassadorTimer = new(Configuration.ServerConfiguration.AmbassadorGaugeDecayRate * 1000) { + AutoReset = true, + Enabled = true + }; + ambassadorTimer.Elapsed += (sender, e) => { + if (!Configuration.ServerConfiguration.AmbassadorGaugeDecayOnlyWhenInRoom || ClientsCount > 0) { + for (int i=0;i<3;i++) ambassadorGauges[i] = Math.Max(0, ambassadorGauges[i]-1); + Send(Utils.VlNetworkPacket(GetRoomVars(), Id)); + } + }; + } + + internal override NetworkArray GetRoomVars() { + NetworkArray vars = new(); + vars.Add(NetworkArray.VlElement("COUNT", (int)Math.Round(ambassadorGauges[0]), isPersistent: true)); + vars.Add(NetworkArray.VlElement("COUNT2", (int)Math.Round(ambassadorGauges[1]), isPersistent: true)); + vars.Add(NetworkArray.VlElement("COUNT3", (int)Math.Round(ambassadorGauges[2]), isPersistent: true)); + for (int i=0;i alerts = new(); + + public void AddAlert(AlertInfo alert) { + alerts.Add(alert); + ResetAlertTimer(alert); + } + + public void SendAllAlerts(Client client) { + return; // Disables joining ongoing alerts (since it doesn't work properly). + + foreach (AlertInfo alert in alerts) { + if (alert.IsRunning()) StartAlert(alert, client); + } + } + + + private void StartAlert(AlertInfo alert, Client? specificClient = null) { + NetworkArray NewRoomVariables = new(); + NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_START, alertId++, isPersistent: true)); + NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_TYPE, alert.type, isPersistent: true)); + double duration = (alert.endTime - DateTime.Now).TotalSeconds; + NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_LENGTH, alert.type == "1" ? alert.redAlertDuration : duration, isPersistent: true)); + if (alert.type == "1") { + NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_TIMEOUT, duration, isPersistent: true)); + } else if (alert.type == "3") { + alert.songId = random.Next(0, alert.songs); + NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_SONG, (double)alert.songId, isPersistent: true)); + } + NetworkPacket packet = Utils.VlNetworkPacket(NewRoomVariables, Id); + if (specificClient is null) { + RoomVariables = NewRoomVariables; + Send(packet); + RoomVariables = new(); + Console.WriteLine("Started event " +alert + " in room " + Name); + } else { + specificClient.Send(packet); + Console.WriteLine("Added " + specificClient.PlayerData.DiplayName + " to event " + alert + " with " + duration + " seconds remaining"); + } + } + + void ResetAlertTimer(AlertInfo alert) { + System.Timers.Timer? timer = alert.timer; + if (timer != null) { + timer.Stop(); + timer.Close(); + } + DateTime startTime = DateTime.Now.AddMilliseconds(random.Next(alert.minTime * 1000, alert.maxTime * 1000)); + DateTime endTime = startTime.AddSeconds(alert.duration); + for (int i = 0; i < alerts.IndexOf(alert); i++) { + // Prevent overlap between two events. + if (alerts[i].Overlaps(endTime)) { + startTime = alerts[i].endTime.AddSeconds(5); + endTime = startTime.AddSeconds(alert.duration); + } + } + timer = new System.Timers.Timer((startTime - DateTime.Now).TotalMilliseconds); + timer.AutoReset = false; + timer.Enabled = true; + timer.Elapsed += (sender, e) => StartAlert(alert); + timer.Elapsed += (sender, e) => ResetAlertTimer(alert); + alert.timer = timer; + Console.WriteLine("Event " + alert + " in " + Name + " scheduled for " + startTime.ToString("MM/dd/yyyy HH:mm:ss tt") + " (in " + (startTime - DateTime.Now).TotalSeconds + " seconds)"); + alert.startTime = startTime; + alert.endTime = endTime; + } + + private const string REDALERT_START = "RA_S"; + private const string REDALERT_TYPE = "RA_A"; + private const string REDALERT_LENGTH = "RA_L"; + private const string REDALERT_TIMEOUT = "RA_T"; + private const string REDALERT_SONG = "RA_SO"; + + public class AlertInfo { + public readonly string type; + public readonly double duration; + public readonly int minTime; + public readonly int maxTime; + public readonly int redAlertDuration; + public readonly int songs; + public int songId; + + public DateTime startTime { + get { + return newStartTime; + } + set { + oldStartTime = newStartTime; + newStartTime = value; + } + } + + public DateTime endTime { + get { + return newEndTime; + } + set { + oldEndTime = newEndTime; + newEndTime = value; + } + } + + private DateTime newStartTime; + private DateTime newEndTime; + private DateTime oldStartTime; + private DateTime oldEndTime; + + public System.Timers.Timer? timer = null; + + public AlertInfo(string type, double duration = 20.0, int minTime = 30, int maxTime = 240, int redAlertDuration = 60, int songs = 16) { + this.type = type; + this.duration = duration; + this.minTime = minTime; + this.maxTime = maxTime; + this.redAlertDuration = redAlertDuration; + this.songs = songs; + } + + public bool Overlaps(DateTime time) { + return (time >= oldStartTime && time <= oldEndTime); + } + + public bool IsRunning() { + return Overlaps(DateTime.Now); + } + + public override string ToString() { + return type switch { + "1" => "RedAlert", + "2" => "DiscoAlert", + "3" => "DanceOff", + _ => type + }; + } + } +} diff --git a/src/Core/UserBanType.cs b/src/Core/UserBanType.cs new file mode 100644 index 0000000..294ba61 --- /dev/null +++ b/src/Core/UserBanType.cs @@ -0,0 +1,10 @@ +namespace sodoffmmo.Core; + +public enum UserBanType +{ + NotBanned = 0, + IndefiniteOpenChatBan = 1, + TemporaryOpenChatBan = 2, + IndefiniteAccountBan = 3, + TemporaryAccountBan = 4 +} diff --git a/src/Core/Utils.cs b/src/Core/Utils.cs new file mode 100644 index 0000000..b9582f5 --- /dev/null +++ b/src/Core/Utils.cs @@ -0,0 +1,56 @@ +using sodoffmmo.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace sodoffmmo.Core; +internal static class Utils { + public static NetworkPacket VlNetworkPacket(NetworkArray vl, int roomID) { + NetworkObject obj = new(); + obj.Add("r", roomID); + obj.Add("vl", vl); + return NetworkObject.WrapObject(0, 11, obj).Serialize(); + } + + public static NetworkPacket VlNetworkPacket(string a, string b, int roomID) { + NetworkArray vl = new(); + vl.Add(NetworkArray.VlElement(a, b)); + return VlNetworkPacket(vl, roomID); + } + + public static NetworkPacket ArrNetworkPacket(string[] data, string c = "", int? roomID = null) { + NetworkObject cmd = new(); + NetworkObject obj = new(); + obj.Add("arr", data); + cmd.Add("c", c); + cmd.Add("p", obj); + NetworkObject ret = NetworkObject.WrapObject(1, 13, cmd); + if (roomID != null) + ret.Add("r", (int)roomID); + return ret.Serialize(); + } + + public static T DeserializeXml(string xmlString) { + var serializer = new XmlSerializer(typeof(T)); + using (var reader = new StringReader(xmlString)) + return (T)serializer.Deserialize(reader); + } + + public static NetworkPacket BuildChatMessage(string uid, string message, string displayName) { + NetworkObject cmd = new(); + NetworkObject data = new(); + data.Add("arr", new string[] { "CMR", "-1", uid, "1", message, "", "1", displayName }); + cmd.Add("c", "CMR"); + cmd.Add("p", data); + + return NetworkObject.WrapObject(1, 13, cmd).Serialize(); + } + + public static NetworkPacket BuildServerSideMessage(string message, string displayName) { + return BuildChatMessage("-1", message, displayName); + } + +} diff --git a/src/Core/WorldEvent.cs b/src/Core/WorldEvent.cs new file mode 100644 index 0000000..933aebf --- /dev/null +++ b/src/Core/WorldEvent.cs @@ -0,0 +1,285 @@ +using System.Globalization; +using sodoffmmo.Data; +using System.Timers; + +namespace sodoffmmo.Core; +class WorldEvent { + enum State { + Active, + End, + NotActive + } + private static WorldEvent? _instance = null; + private static object EventLock = new object(); + private Random random = new Random(); + private System.Timers.Timer? timer = null; + + public static WorldEvent Get() { + lock(EventLock) { + if (_instance == null) { + _instance = new WorldEvent(); + } + return _instance; + } + } + + private WorldEvent() { + startTime = DateTime.UtcNow.AddMinutes(-60); + startTimeString = startTime.ToString("MM/dd/yyyy HH:mm:ss"); + room = Room.GetOrAdd("HubTrainingDO"); + uid = "sodoff"; + state = State.End; + ScheduleEvent(Configuration.ServerConfiguration.FirstEventTimer); // WE_ != WEN_ + } + + // controlled (init/reset) by Reset() + private Room room; + private string uid; + private Client? operatorAI; + private State state; + + private DateTime startTime; + private DateTime endTime; + private DateTime nextStartTime; + private DateTime AITime; + private bool endTimeIsSet; + private string startTimeString; + private string nextStartTimeString; + + // controlled (init/reset) by InitEvent() + private Dictionary health = new(); + private Dictionary players = new(); + private string lastResults = ""; + + // reset event - set new id, start time, end time, etc + private void Reset(DateTime newStartTime) { + lock (EventLock) { + uid = Path.GetRandomFileName().Substring(0, 8); // this is used as RandomSeed for random select ship variant + operatorAI = null; + state = State.NotActive; + + startTime = newStartTime; + startTimeString = startTime.ToString("MM/dd/yyyy HH:mm:ss"); + AITime = startTime.AddMinutes(-1); + UpdateEndTime(600 + 90); + endTimeIsSet = false; + + nextStartTime = startTime; + nextStartTimeString = startTimeString; + + Console.WriteLine($"Event {uid} start time: {startTimeString}"); + } + } + + // set / update event end time in results of Reset() or SetTimeSpan() + private void UpdateEndTime(double timeout) { + endTime = startTime.AddSeconds(timeout); + Console.WriteLine($"Event {uid} end time: {endTime}"); + SetTimer((endTime - DateTime.UtcNow).TotalSeconds, PreEndEvent); + } + + // schedule next event and set timer to call PreInit + private void ScheduleEvent(float minutes = 0) { + if (minutes > 2) + nextStartTime = DateTime.UtcNow.AddMinutes(minutes); + else + nextStartTime = startTime.AddMinutes(Configuration.ServerConfiguration.EventTimer); + nextStartTimeString = nextStartTime.ToString("MM/dd/yyyy HH:mm:ss"); + + double timeout = (nextStartTime - DateTime.UtcNow).TotalSeconds - 120; + if (timeout > 0) + SetTimer((nextStartTime - DateTime.UtcNow).TotalSeconds - 120, PreInit); + else + Console.WriteLine($"Events disabled"); + } + + // reset event and set timer to call PreEndEvent, send new WE_ info + private void PreInit(Object? source, ElapsedEventArgs e) { + Reset(nextStartTime); // WE_ == WEN_ + AnnounceEvent(); + } + + // check init state and init event (set AI, reset health, score) if need in response to client (shot, etc) request + private void InitEvent() { + lock (EventLock) { + if (AITime < DateTime.UtcNow && state != State.End) { + var clients = room.Clients.ToList(); + operatorAI = clients[random.Next(0, clients.Count)]; + AITime = DateTime.UtcNow.AddSeconds(3.5); + + if (state == State.NotActive) { + // clear here because after Reset() we can get late packages about previous events + health = new(); + players = new(); + lastResults = ""; + state = State.Active; + } + + operatorAI.Send(Utils.VlNetworkPacket("WE__AI", operatorAI.PlayerData.Uid, room.Id)); + Console.WriteLine($"Event {uid} AI operator: {operatorAI.PlayerData.Uid}"); + } + } + } + + private void PreEndEvent(Object? source, ElapsedEventArgs e) { + Console.WriteLine($"Event {uid} force end from timer"); + EndEvent(true); + } + + private bool EndEvent(bool force = false) { + bool results = false; + string targets = ""; + if (health.Count > 0) { + results = true; + foreach (var x in health) { + results = results && (x.Value == 0.0f); + targets += x.Key + ":" + x.Value.ToString("0.0#####", CultureInfo.GetCultureInfo("en-US")) + ","; + } + } + if (results || force) { + lock (EventLock) { + if (state == State.End || (state == State.NotActive && !force)) + return true; + state = State.End; + } + + string scores = ""; + foreach (var x in players) { + scores += x.Key + "/" + x.Value + ","; + } + lastResults = $"{uid};{results};{scores};{targets}"; + + Console.WriteLine($"Event {uid} end: {results} {targets} {scores}"); + + SetTimer(2, PostEndEvent1); // looks like client don't like get _End before WEH_ with 0.0 ... so wait to send _End + + return true; + } + return false; + } + + // send reward info + private void PostEndEvent1(Object? source, ElapsedEventArgs e) { + NetworkArray arr = new(); + + arr.Add(NetworkArray.VlElement("WE_" + Configuration.ServerConfiguration.EventName + "_End", lastResults)); + arr.Add(NetworkArray.VlElement("WE_" + Configuration.ServerConfiguration.EventName, NetworkArray.NULL)); + arr.Add(NetworkArray.VlElement("WE__AI", NetworkArray.NULL)); + foreach (var t in health) { + arr.Add(NetworkArray.VlElement("WEH_" + t.Key, NetworkArray.NULL)); + arr.Add(NetworkArray.VlElement("WEF_" + t.Key, NetworkArray.NULL)); + } + + room.Send(Utils.VlNetworkPacket(arr, room.Id)); + + Console.WriteLine($"Event {uid} sent _End"); + + SetTimer(60, PostEndEvent2); + } + + // schedule next event, set timer to call PreInit() and send new WEN_ info + private void PostEndEvent2(Object? source, ElapsedEventArgs e) { + ScheduleEvent(); // WE_ != WEN_ + AnnounceEvent(false, true); // send only WEN_ (WE_ should stay unchanged ... as WE_..._End) + } + + // set server side timer for word event state changes + private void SetTimer(double timeout, System.Timers.ElapsedEventHandler callback) { + if (timer != null) { + timer.Stop(); + timer.Close(); + } + + timer = new System.Timers.Timer(timeout * 1000); + timer.AutoReset = false; + timer.Enabled = true; + timer.Elapsed += callback; + + Console.WriteLine($"Event timer {callback.Method.Name} set to {timeout} s"); + } + + // send event info + private void AnnounceEvent(bool WE = true, bool WEN = true) { + Console.WriteLine($"Event {uid} send event notification (WE_ = {(WE ? startTimeString : WE)} WEN_ = {(WEN ? nextStartTimeString : WEN)}, room = {room.Id}) to all clients"); + NetworkPacket packet = Utils.VlNetworkPacket(EventInfoArray(WE, WEN), room.Id); + foreach (var r in Room.AllRooms()) { + r.Send(packet); + } + } + + public string EventInfo() { + return startTimeString + "," + uid + ", false, HubTrainingDO"; + } + + public NetworkArray EventInfoArray(bool WE = true, bool WEN = true) { + NetworkArray vl = new(); + if (WE) { + vl.Add(NetworkArray.VlElement("WE_" + Configuration.ServerConfiguration.EventName, EventInfo(), isPersistent:true)); + } + if (WEN) { + vl.Add(NetworkArray.VlElement("WEN_" + Configuration.ServerConfiguration.EventName, nextStartTimeString, isPersistent:true)); + } + return vl; + } + + public string GetUid() => uid; + + public Room GetRoom() => room; + + public float UpdateHealth(string targetUid, float updateVal) { + InitEvent(); + + lock (EventLock) { + if (state != State.Active) { + Console.WriteLine($"Event {uid} reject UpdateHealth for {targetUid} with event state {state}"); + return -1.0f; // do not send WEH_ when event is not active + } + } + + if (!health.ContainsKey(targetUid)) + health.Add(targetUid, 1.0f); + health[targetUid] -= updateVal; + + if (health[targetUid] < 0.0001f) { + health[targetUid] = 0.0f; + EndEvent(); + } + + if (endTime < DateTime.UtcNow) { + Console.WriteLine($"Event {uid} force end from UpdateHealth"); + EndEvent(true); + } + + return health[targetUid]; + } + + public void SetTimeSpan(Client client, float seconds) { + if (state != State.Active) { + return; + } + if (client == operatorAI || !endTimeIsSet) { + Console.WriteLine($"Event {uid} set TimeSpan: {seconds} from operator: {client == operatorAI}"); + UpdateEndTime(seconds); + endTimeIsSet = true; + } + } + + public void UpdateScore(string client, string value) { + if (!players.ContainsKey(client)) { + players.Add(client, value); + } else { + players[client] = value; + } + } + + public void UpdateAI(Client client) { + if (client == operatorAI) + AITime = DateTime.UtcNow.AddSeconds(7); + } + + public float GetHealth(string targetUid) => health[targetUid]; + + public bool IsActive() => (state == State.Active); + + public string GetLastResults() => lastResults; +} diff --git a/src/Data/DataDecoder.cs b/src/Data/DataDecoder.cs new file mode 100644 index 0000000..64ee027 --- /dev/null +++ b/src/Data/DataDecoder.cs @@ -0,0 +1,55 @@ +namespace sodoffmmo.Data; + +internal static class DataDecoder { + internal static DataWrapper DecodeNull(NetworkData data) => new DataWrapper(NetworkDataType.Null, null!); + + internal static DataWrapper DecodeBool(NetworkData data) => new DataWrapper(NetworkDataType.Bool, data.ReadBool()); + + internal static DataWrapper DecodeByte(NetworkData data) => new DataWrapper(NetworkDataType.Byte, data.ReadByte()); + + internal static DataWrapper DecodeShort(NetworkData data) => new DataWrapper(NetworkDataType.Short, data.ReadShort()); + + internal static DataWrapper DecodeInt(NetworkData data) => new DataWrapper(NetworkDataType.Int, data.ReadInt()); + + internal static DataWrapper DecodeLong(NetworkData data) => new DataWrapper(NetworkDataType.Long, data.ReadLong()); + + internal static DataWrapper DecodeFloat(NetworkData data) => new DataWrapper(NetworkDataType.Float, data.ReadFloat()); + + internal static DataWrapper DecodeDouble(NetworkData data) => new DataWrapper(NetworkDataType.Double, data.ReadDouble()); + + internal static DataWrapper DecodeString(NetworkData data) => new DataWrapper(NetworkDataType.String, data.ReadString()); + + internal static DataWrapper DecodeByteArray(NetworkData data) { + int count = data.ReadInt(); + return new DataWrapper(NetworkDataType.ByteArray, data.ReadChunk(count)); + } + + internal static DataWrapper DecodeBoolArray(NetworkData data) + => new DataWrapper(NetworkDataType.BoolArray, DecodeTypedArray(data, d => d.ReadBool())); + + internal static DataWrapper DecodeShortArray(NetworkData data) + => new DataWrapper(NetworkDataType.ShortArray, DecodeTypedArray(data, d => d.ReadShort())); + + internal static DataWrapper DecodeIntArray(NetworkData data) + => new DataWrapper(NetworkDataType.IntArray, DecodeTypedArray(data, d => d.ReadInt())); + + internal static DataWrapper DecodeLongArray(NetworkData data) + => new DataWrapper(NetworkDataType.LongArray, DecodeTypedArray(data, d => d.ReadLong())); + + internal static DataWrapper DecodeFloatArray(NetworkData data) + => new DataWrapper(NetworkDataType.FloatArray, DecodeTypedArray(data, d => d.ReadFloat())); + + internal static DataWrapper DecodeDoubleArray(NetworkData data) + => new DataWrapper(NetworkDataType.DoubleArray, DecodeTypedArray(data, d => d.ReadDouble())); + + internal static DataWrapper DecodeStringArray(NetworkData data) + => new DataWrapper(NetworkDataType.IntArray, DecodeTypedArray(data, d => d.ReadString())); + + private static T[] DecodeTypedArray(NetworkData data, Func readFunction) { + short length = data.ReadShort(); + T[] arr = new T[length]; + for (short i = 0; i < length; i++) + arr[i] = readFunction(data); + return arr; + } +} diff --git a/src/Data/DataEncoder.cs b/src/Data/DataEncoder.cs new file mode 100644 index 0000000..9d005f5 --- /dev/null +++ b/src/Data/DataEncoder.cs @@ -0,0 +1,132 @@ +namespace sodoffmmo.Data; +internal static class DataEncoder { + internal static byte[] EncodeNull() => new byte[] { 0 }; + + internal static byte[] EncodeByte(byte value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.Byte); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeBool(bool value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.Bool); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeShort(short value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.Short); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeInt(int value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.Int); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeLong(long value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.Long); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeFloat(float value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.Float); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeDouble(double value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.Double); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeString(string value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.String); + data.WriteValue(value); + return data.Data; + } + + internal static byte[] EncodeBoolArray(bool[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.BoolArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } + + internal static byte[] EncodeByteArray(byte[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.ByteArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } + + internal static byte[] EncodeShortArray(short[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.ShortArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } + + internal static byte[] EncodeIntArray(int[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.IntArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } + + internal static byte[] EncodeLongArray(long[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.LongArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } + + internal static byte[] EncodeFloatArray(float[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.FloatArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } + + internal static byte[] EncodeDoubleArray(double[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.DoubleArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } + + internal static byte[] EncodeStringArray(string[] value) { + NetworkData data = new(); + data.WriteValue((byte)NetworkDataType.StringArray); + data.WriteValue(Convert.ToInt16(value.Length)); + for (int i = 0; i < value.Length; i++) + data.WriteValue(value[i]); + return data.Data; + } +} diff --git a/src/Data/DataWrapper.cs b/src/Data/DataWrapper.cs new file mode 100644 index 0000000..a013c49 --- /dev/null +++ b/src/Data/DataWrapper.cs @@ -0,0 +1,11 @@ +namespace sodoffmmo.Data; +public class DataWrapper { + + public int Type { get; private set; } + public object Data { get; private set; } + + public DataWrapper(NetworkDataType type, object data) { + this.Type = (int)type; + this.Data = data; + } +} diff --git a/src/Data/NetworkArray.cs b/src/Data/NetworkArray.cs new file mode 100644 index 0000000..61992d8 --- /dev/null +++ b/src/Data/NetworkArray.cs @@ -0,0 +1,144 @@ +namespace sodoffmmo.Data; +public class NetworkArray { + List arrData = new(); + + public DataWrapper this[int i] { + get { return arrData[i]; } + } + + public int Length { + get { return arrData.Count; } + } + + public void Add(bool value) => AddObject(NetworkDataType.Bool, value); + + public void Add(byte value) => AddObject(NetworkDataType.Byte, value); + + public void Add(short value) => AddObject(NetworkDataType.Short, value); + + public void Add(int value) => AddObject(NetworkDataType.Int, value); + + public void Add(long value) => AddObject(NetworkDataType.Long, value); + + public void Add(float value) => AddObject(NetworkDataType.Float, value); + + public void Add(double value) => AddObject(NetworkDataType.Double, value); + + public void Add(string value) => AddObject(NetworkDataType.String, value); + + public void Add(bool[] value) => AddObject(NetworkDataType.BoolArray, value); + + public void Add(NetworkData value) => AddObject(NetworkDataType.ByteArray, value.Data); + + public void Add(short[] value) => AddObject(NetworkDataType.ShortArray, value); + + public void Add(int[] value) => AddObject(NetworkDataType.IntArray, value); + + public void Add(long[] value) => AddObject(NetworkDataType.LongArray, value); + + public void Add(float[] value) => AddObject(NetworkDataType.FloatArray, value); + + public void Add(double[] value) => AddObject(NetworkDataType.DoubleArray, value); + + public void Add(string[] value) => AddObject(NetworkDataType.StringArray, value); + + public void Add(NetworkArray value) => AddObject(NetworkDataType.NetworkArray, value); + + public void Add(NetworkObject value) => AddObject(NetworkDataType.NetworkObject, value); + + public void Add(DataWrapper dataWrapper) => arrData.Add(dataWrapper); + + private void AddObject(NetworkDataType dataType, object obj) => Add(new DataWrapper(dataType, obj)); + + public T GetValue(int index) { + if (index >= arrData.Count) + throw new IndexOutOfRangeException(); + return (T)arrData[index].Data; + } + + public bool GetBool(int index) => GetValue(index); + + public byte GetByte(int index) => GetValue(index); + + public short GetShort(int index) => GetValue(index); + + public int GetInt(int index) => GetValue(index); + + public long GetLong(int index) => GetValue(index); + + public float GetFloat(int index) => GetValue(index); + + public double GetDouble(int index) => GetValue(index); + + public string GetUtfString(int index) => GetValue(index); + + public bool[] GetBoolArray(int index) => GetValue(index); + + public NetworkData GetNetworkData(int index) => GetValue(index); + + public short[] GetShortArray(int index) => GetValue(index); + + public int[] GetIntArray(int index) => GetValue(index); + + public long[] GetLongArray(int index) => GetValue(index); + + public float[] GetFloatArray(int index) => GetValue(index); + + public double[] GetDoubleArray(int index) => GetValue(index); + + public string[] GetStringArray(int index) => GetValue(index); + + public NetworkArray GetNetworkArray(int index) => GetValue(index); + + public NetworkObject GetNetworkObject(int index) => GetValue(index); + + public bool Contains(object obj) { + if (obj is NetworkObject || obj is NetworkArray) + throw new Exception("Unsupported object type"); + for (int i = 0; i < arrData.Count; i++) { + if (object.Equals(arrData[i], obj)) + return true; + } + return false; + } + + public class NullClass {}; + public static NullClass NULL = new (); + + public void AddWithType(T value) { + if (typeof(T) == typeof(NullClass)) + AddWithTypeObject(NetworkDataType.Null, 0, null); + else if (typeof(T) == typeof(bool)) + AddWithTypeObject(NetworkDataType.Bool, 1, value); + else if (typeof(T) == typeof(int)) + AddWithTypeObject(NetworkDataType.Int, 2, value); + else if (typeof(T) == typeof(double)) + AddWithTypeObject(NetworkDataType.Double, 3, value); + else if (typeof(T) == typeof(float)) + AddWithTypeObject(NetworkDataType.Float, 3, value); + else if (typeof(T) == typeof(string)) + AddWithTypeObject(NetworkDataType.String, 4, value); + else + throw new Exception("Unsupported type"); + } + + private void AddWithTypeObject(NetworkDataType dataType, byte typeId, object? obj) { + Add(new DataWrapper(NetworkDataType.Byte, typeId)); + Add(new DataWrapper(dataType, obj)); + } + + public static NetworkArray Param(string name, T value) { + NetworkArray arr = new(); + arr.Add(name); + arr.AddWithType(value); + return arr; + } + public static NetworkArray VlElement(string name, T value, bool isPrivate = false, bool isPersistent = false) { + NetworkArray arr = new(); + arr.Add(name); + arr.AddWithType(value); + arr.Add(isPrivate); + arr.Add(isPersistent); + return arr; + } +} diff --git a/src/Data/NetworkData.cs b/src/Data/NetworkData.cs new file mode 100644 index 0000000..b861d74 --- /dev/null +++ b/src/Data/NetworkData.cs @@ -0,0 +1,131 @@ +using System.Text; + +namespace sodoffmmo.Data; +public class NetworkData { + byte[] data; + int offset = 0; + + public byte[] Data { + get { return data; } + } + + public NetworkData() { + data = new byte[0]; + } + + public NetworkData(byte[] data) { + this.data = data; + } + + public int RemainingLength { + get { + return data.Length - offset; + } + } + + public void Seek(int offset) { + int newOffset = this.offset + offset; + if (newOffset >= 0 && newOffset < data.Length) + this.offset = newOffset; + } + + public byte[] ReverseOrder(byte[] data) { + if (!BitConverter.IsLittleEndian) return data; + Array.Reverse(data); + return data; + } + + public byte ReadByte() { + return data[offset++]; + } + public byte[] ReadChunk(int count) { + byte[] chunk = new byte[count]; + Buffer.BlockCopy(data, offset, chunk, 0, count); + offset += count; + return chunk; + } + + public short ReadShort() { + byte[] arr = ReverseOrder(ReadChunk(2)); + return BitConverter.ToInt16(arr); + } + + public ushort ReadUShort() { + byte[] arr = ReverseOrder(ReadChunk(2)); + return BitConverter.ToUInt16(arr); + } + + public bool ReadBool() => data[offset++] == 1; + + public int ReadInt() { + byte[] arr = ReverseOrder(ReadChunk(4)); + return BitConverter.ToInt32(arr); + } + + public long ReadLong() { + byte[] arr = ReverseOrder(ReadChunk(8)); + return BitConverter.ToInt64(arr); + } + + public float ReadFloat() { + byte[] arr = ReverseOrder(ReadChunk(4)); + return BitConverter.ToSingle(arr); + } + + public double ReadDouble() { + byte[] arr = ReverseOrder(ReadChunk(8)); + return BitConverter.ToDouble(arr); + } + + public string ReadString() { + ushort count = ReadUShort(); + string str = Encoding.UTF8.GetString(data, offset, count); + offset += count; + return str; + } + + public void WriteChunk(byte[] chunk, int offset, int count) { + byte[] newData = new byte[data.Length + count]; + Buffer.BlockCopy(data, 0, newData, 0, data.Length); + Buffer.BlockCopy(chunk, offset, newData, data.Length, count); + data = newData; + } + + public void WriteChunk(byte[] chunk) => WriteChunk(chunk, 0, chunk.Length); + + public void WriteValue(byte b) => WriteChunk(new byte[] { b }); + + public void WriteValue(bool b) => WriteChunk(new byte[] { (byte)((!b) ? 0 : 1) }); + + public void WriteValue(int i) => WriteChunk(ReverseOrder(BitConverter.GetBytes(i))); + + public void WriteValue(short s) => WriteChunk(ReverseOrder(BitConverter.GetBytes(s))); + + public void WriteValue(ushort us) => WriteChunk(ReverseOrder(BitConverter.GetBytes(us))); + + public void WriteValue(long l) => WriteChunk(ReverseOrder(BitConverter.GetBytes(l))); + + public void WriteValue(float f) => WriteChunk(ReverseOrder(BitConverter.GetBytes(f))); + + public void WriteValue(double d) => WriteChunk(ReverseOrder(BitConverter.GetBytes(d))); + + public void WriteValue(string str) { + WriteValue(GetUTFStringLength(str)); + WriteChunk(Encoding.UTF8.GetBytes(str)); + } + + private ushort GetUTFStringLength(string str) { + ushort length = 0; + foreach (int c in str) { + if (c > 0 && c < 128) + ++length; + else if (c > 2047) + length += 3; + else + length += 2; + } + if (length > 32768) + throw new Exception("String is too long"); + return length; + } +} diff --git a/src/Data/NetworkDataType.cs b/src/Data/NetworkDataType.cs new file mode 100644 index 0000000..b13d4ae --- /dev/null +++ b/src/Data/NetworkDataType.cs @@ -0,0 +1,23 @@ +namespace sodoffmmo.Data; + +public enum NetworkDataType { + Null, + Bool, + Byte, + Short, + Int, + Long, + Float, + Double, + String, + BoolArray, + ByteArray, + ShortArray, + IntArray, + LongArray, + FloatArray, + DoubleArray, + StringArray, + NetworkArray, + NetworkObject, +} diff --git a/src/Data/NetworkObject.cs b/src/Data/NetworkObject.cs new file mode 100644 index 0000000..1605c66 --- /dev/null +++ b/src/Data/NetworkObject.cs @@ -0,0 +1,200 @@ +namespace sodoffmmo.Data; +public class NetworkObject { + Dictionary fields = new(); + + public NetworkObject() {} + + public NetworkObject(NetworkData data) { + Deserialize(data); + } + + public NetworkObject(byte[] data) : this(new NetworkData(data)) { } + + public void Add(string label, DataWrapper wrapper) => fields[label] = wrapper; + + public void Add(string label, byte value) => fields[label] = new DataWrapper(NetworkDataType.Byte, value); + + public void Add(string label, bool value) => fields[label] = new DataWrapper(NetworkDataType.Bool, value); + + public void Add(string label, short value) => fields[label] = new DataWrapper(NetworkDataType.Short, value); + + public void Add(string label, int value) => fields[label] = new DataWrapper(NetworkDataType.Int, value); + + public void Add(string label, long value) => fields[label] = new DataWrapper(NetworkDataType.Long, value); + + public void Add(string label, float value) => fields[label] = new DataWrapper(NetworkDataType.Float, value); + + public void Add(string label, double value) => fields[label] = new DataWrapper(NetworkDataType.Double, value); + + public void Add(string label, string value) => fields[label] = new DataWrapper(NetworkDataType.String, value); + + public void Add(string label, byte[] value) => fields[label] = new DataWrapper(NetworkDataType.ByteArray, value); + + public void Add(string label, bool[] value) => fields[label] = new DataWrapper(NetworkDataType.BoolArray, value); + + public void Add(string label, short[] value) => fields[label] = new DataWrapper(NetworkDataType.ShortArray, value); + + public void Add(string label, int[] value) => fields[label] = new DataWrapper(NetworkDataType.IntArray, value); + + public void Add(string label, long[] value) => fields[label] = new DataWrapper(NetworkDataType.LongArray, value); + + public void Add(string label, float[] value) => fields[label] = new DataWrapper(NetworkDataType.FloatArray, value); + + public void Add(string label, double[] value) => fields[label] = new DataWrapper(NetworkDataType.DoubleArray, value); + + public void Add(string label, string[] value) => fields[label] = new DataWrapper(NetworkDataType.StringArray, value); + + public void Add(string label, NetworkArray value) => fields[label] = new DataWrapper(NetworkDataType.NetworkArray, value); + + public void Add(string label, NetworkObject value) => fields[label] = new DataWrapper(NetworkDataType.NetworkObject, value); + + public T Get(string key) { + if (!fields.ContainsKey(key)) + return default; + return (T)fields[key].Data; + } + + + public NetworkPacket Serialize() => new NetworkPacket(0x80, SerializeObject(this)); + + private byte[] SerializeObject(NetworkObject obj) { + NetworkData data = new(); + data.WriteValue((byte)18); + data.WriteValue(Convert.ToInt16(obj.fields.Count)); + foreach (string key in obj.fields.Keys) { + data.WriteValue(key); + data.WriteChunk(EncodeObject(obj.fields[key])); + } + return data.Data; + } + + private void Deserialize(NetworkData data) { + if (data.ReadByte() != 0x12) + throw new Exception("Invalid object type"); + + short count = data.ReadShort(); + for (short i = 0; i < count; i++) { + string label = data.ReadString(); + DataWrapper obj = DecodeObject(data); + Add(label, obj); + } + } + + private byte[] EncodeObject(DataWrapper obj) { + switch ((NetworkDataType)obj.Type) { + case NetworkDataType.Null: + return DataEncoder.EncodeNull(); + case NetworkDataType.Bool: + return DataEncoder.EncodeBool((bool)obj.Data); + case NetworkDataType.Byte: + return DataEncoder.EncodeByte((byte)obj.Data); + case NetworkDataType.Short: + return DataEncoder.EncodeShort((short)obj.Data); + case NetworkDataType.Int: + return DataEncoder.EncodeInt((int)obj.Data); + case NetworkDataType.Long: + return DataEncoder.EncodeLong((long)obj.Data); + case NetworkDataType.Float: + return DataEncoder.EncodeFloat((float)obj.Data); + case NetworkDataType.Double: + return DataEncoder.EncodeDouble((double)obj.Data); + case NetworkDataType.String: + return DataEncoder.EncodeString((string)obj.Data); + case NetworkDataType.BoolArray: + return DataEncoder.EncodeBoolArray((bool[])obj.Data); + case NetworkDataType.ByteArray: + return DataEncoder.EncodeByteArray((byte[])obj.Data); + case NetworkDataType.ShortArray: + return DataEncoder.EncodeShortArray((short[])obj.Data); + case NetworkDataType.IntArray: + return DataEncoder.EncodeIntArray((int[])obj.Data); + case NetworkDataType.LongArray: + return DataEncoder.EncodeLongArray((long[])obj.Data); + case NetworkDataType.FloatArray: + return DataEncoder.EncodeFloatArray((float[])obj.Data); + case NetworkDataType.DoubleArray: + return DataEncoder.EncodeDoubleArray((double[])obj.Data); + case NetworkDataType.StringArray: + return DataEncoder.EncodeStringArray((string[])obj.Data); + case NetworkDataType.NetworkArray: + return EncodeNetworkArray((NetworkArray)obj.Data); + case NetworkDataType.NetworkObject: + return SerializeObject((NetworkObject)obj.Data); + default: throw new Exception("Invalid object"); + } + } + + private byte[] EncodeNetworkArray(NetworkArray arr) { + NetworkData data = new(); + data.WriteValue((byte)17); + data.WriteValue(Convert.ToInt16(arr.Length)); + for (int i = 0; i < arr.Length; i++) + data.WriteChunk(EncodeObject(arr[i])); + return data.Data; + } + + private DataWrapper DecodeObject(NetworkData data) { + switch ((NetworkDataType)data.ReadByte()) { + case NetworkDataType.Null: + return DataDecoder.DecodeNull(data); + case NetworkDataType.Bool: + return DataDecoder.DecodeBool(data); + case NetworkDataType.Byte: + return DataDecoder.DecodeByte(data); + case NetworkDataType.Short: + return DataDecoder.DecodeShort(data); + case NetworkDataType.Int: + return DataDecoder.DecodeInt(data); + case NetworkDataType.Long: + return DataDecoder.DecodeLong(data); + case NetworkDataType.Float: + return DataDecoder.DecodeFloat(data); + case NetworkDataType.Double: + return DataDecoder.DecodeDouble(data); + case NetworkDataType.String: + return DataDecoder.DecodeString(data); + case NetworkDataType.BoolArray: + return DataDecoder.DecodeBoolArray(data); + case NetworkDataType.ByteArray: + return DataDecoder.DecodeByteArray(data); + case NetworkDataType.ShortArray: + return DataDecoder.DecodeShortArray(data); + case NetworkDataType.IntArray: + return DataDecoder.DecodeIntArray(data); + case NetworkDataType.LongArray: + return DataDecoder.DecodeLongArray(data); + case NetworkDataType.FloatArray: + return DataDecoder.DecodeFloatArray(data); + case NetworkDataType.DoubleArray: + return DataDecoder.DecodeDoubleArray(data); + case NetworkDataType.StringArray: + return DataDecoder.DecodeStringArray(data); + case NetworkDataType.NetworkArray: + return DecodeNetworkArray(data); + case NetworkDataType.NetworkObject: + data.Seek(-1); + return new DataWrapper(NetworkDataType.NetworkObject, new NetworkObject(data)); + default: throw new Exception("Invalid object"); + } + } + + private DataWrapper DecodeNetworkArray(NetworkData data) { + NetworkArray array = new NetworkArray(); + short count = data.ReadShort(); + for (short i = 0; i < count; i++) { + DataWrapper wrapper = DecodeObject(data); + array.Add(wrapper); + } + return new DataWrapper(NetworkDataType.NetworkArray, array); + } + + public static NetworkObject WrapObject(byte c, short a, NetworkObject obj) { + NetworkObject wrapper = new(); + wrapper.Add("c", c); + wrapper.Add("a", a); + wrapper.Add("p", obj); + return wrapper; + } + + public bool ContainsKey(string label) => fields.ContainsKey(label); +} diff --git a/src/Data/NetworkPacket.cs b/src/Data/NetworkPacket.cs new file mode 100644 index 0000000..329a211 --- /dev/null +++ b/src/Data/NetworkPacket.cs @@ -0,0 +1,76 @@ +using ComponentAce.Compression.Libs.zlib; + +namespace sodoffmmo.Data; +public class NetworkPacket { + + byte header; + byte[] data; + bool compressed = false; + + public int Length { + get { + return data.Length; + } + } + + public byte[] SendData { + get { + NetworkData sendData = new(); + sendData.WriteValue(header); + sendData.WriteValue((short)data.Length); + sendData.WriteChunk(data); + return sendData.Data; + } + } + + public NetworkPacket() { + data = new byte[0]; + } + + public NetworkPacket(NetworkData data, bool compressed = false) { + header = 0x80; + if (compressed) { + header = 0xa0; + this.compressed = true; + } + this.data = new byte[data.Data.Length]; + Buffer.BlockCopy(data.Data, 0, this.data, 0, data.Data.Length); + } + + public NetworkPacket(byte header, byte[] data) { + this.header = header; + this.data = new byte[data.Length]; + Buffer.BlockCopy(data, 0, this.data, 0, data.Length); + if (header == 0xa0) + compressed = true; + } + + public NetworkObject GetObject() { + if (compressed) + Decompress(); + + NetworkObject obj = new NetworkObject(data); + return obj; + } + + public void Compress() { + if (compressed) + return; + MemoryStream outStream = new(); + using (ZOutputStream zstream = new(outStream, 9)) { + zstream.Write(data); + zstream.Flush(); + } + data = outStream.ToArray(); + header = 0xa0; + } + + public void Decompress() { + MemoryStream outStream = new(); + using (ZOutputStream zstream = new(outStream)) { + zstream.Write(data); + zstream.Flush(); + } + data = outStream.ToArray(); + } +} diff --git a/src/Data/PlayerData.cs b/src/Data/PlayerData.cs new file mode 100644 index 0000000..d2b7d7c --- /dev/null +++ b/src/Data/PlayerData.cs @@ -0,0 +1,228 @@ +using System.Text.RegularExpressions; +using System.Globalization; +using sodoffmmo.Core; +using sodoffmmo.Management; + +namespace sodoffmmo.Data; +public class PlayerData { + public bool IsValid { get; set; } = false; + + // viking uid + public string Uid { get; set; } = ""; + // client token + public string UNToken { get; set; } = ""; + // client zone + public string ZoneName { get; set; } = ""; + + // rotation (eulerAngles.y) + public float R { get; set; } + // velocity x + public float R1 { get; set; } + // velocity y + public float R2 { get; set; } + // velocity z + public float R3 { get; set; } + // position x + public float P1 { get; set; } + // position y + public float P2 { get; set; } + // position z + public float P3 { get; set; } + // max speed + public float Mx { get; set; } = 6; + // flags + public int F { get; set; } + // animation bitfield (animations used by avatar, e.g. mounted, swim, ...) + public int Mbf { get; set; } + + public string DiplayName { get; set; } = "placeholder"; + public Role Role { get; set; } = Role.User; + + public long last_ue_time { get; set; } = 0; + + public static readonly string[] SupportedVariables = { + "A", // avatar data + "FP", // raised pet data + "RA", // XP rank (points and level) + "UDT", // UDT points + "L", // location (level) + "PU", // (not raised) pet data + "RDE", // ride (int) + "MU", // mood + "MBR", // mount broom (bool) + "GU", // goggles (bool) + "LC", // livechat (int) + "CU", // country id (int) (for flag?) + "J", // join allowed + "BU", // busy (?) + "M", // membership status (bool) + "P", // position vector (older games) + "R", // rotation (older games - updated via SUV, not SPV) + "F", // flags (older games - updated via SUV, not SPV) + "UTI", // group/clan (EMD) + "CLU", // group/clan (newer) + "SPM", // drive mode (Eat My Dust) + "H", // health + }; + + // other variables (set and updated via SUV command) + private Dictionary variables = new(); + + public string GetVariable(string varName) { + return variables[varName]; + } + + public string SetVariable(string varName, string value) { + // do not store in variables directory + if (varName == "UID") { + return value; + } + if (varName == "R") { + R = float.Parse(value, CultureInfo.InvariantCulture); + return value; + } + if (varName == "F") { + F = unchecked((int)Convert.ToUInt32(value, 16)); + return value; + } + + // fix variable value before store + if (varName == "FP") { + value = FixMountState(value); + } + + // store in directory + variables[varName] = value; + return value; + } + + public void InitFromNetworkData(NetworkObject suvData) { + // set initial state for SPV data + string? r = suvData.Get("R"); // in Eat My Dust, rotation is sent in R1, R2, R3 + if (r != null) R = float.Parse(r, CultureInfo.InvariantCulture); + string? p1 = suvData.Get("P1"); + if (p1 != null) { + P1 = float.Parse(p1, CultureInfo.InvariantCulture); + P2 = float.Parse(suvData.Get("P2"), CultureInfo.InvariantCulture); + P3 = float.Parse(suvData.Get("P3"), CultureInfo.InvariantCulture); + R1 = float.Parse(suvData.Get("R1"), CultureInfo.InvariantCulture); + R2 = float.Parse(suvData.Get("R2"), CultureInfo.InvariantCulture); + R3 = float.Parse(suvData.Get("R3"), CultureInfo.InvariantCulture); + } + string? mbf = suvData.Get("MBF"); + if (mbf != null) + Mbf = int.Parse(mbf); + F = int.Parse(suvData.Get("F")); + + // reset all variables values + // variables.Clear(); + + // set initial state for SUV data + foreach (string varName in SupportedVariables) { + string? value = suvData.Get(varName); + if (value != null) { + SetVariable(varName, value); + } + } + + IsValid = true; + } + + public NetworkArray GetNetworkData(int clientID, out NetworkArray paramArr) { + NetworkArray arr = new(); + arr.Add(clientID); + arr.Add(UNToken); + arr.Add((short)1); + arr.Add((short)clientID); + + paramArr = new(); + paramArr.Add(NetworkArray.Param("NT", (double)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()))); // network time (like PNG) + paramArr.Add(NetworkArray.Param("t", (int)(Runtime.CurrentRuntime / 1000))); // timestamp (non-decreasing integer) + + paramArr.Add(NetworkArray.Param("UID", Uid)); + addVariableToArray(paramArr, "A"); + addVariableToArray(paramArr, "FP"); + + if (IsValid) { + paramArr.Add(NetworkArray.Param("R", R)); + paramArr.Add(NetworkArray.Param("R1", R1)); + paramArr.Add(NetworkArray.Param("R2", R2)); + paramArr.Add(NetworkArray.Param("R3", R3)); + paramArr.Add(NetworkArray.Param("P1", P1)); + paramArr.Add(NetworkArray.Param("P2", P2)); + paramArr.Add(NetworkArray.Param("P3", P3)); + paramArr.Add(NetworkArray.Param("MX", Mx)); + paramArr.Add(NetworkArray.Param("F", F)); + paramArr.Add(NetworkArray.Param("MBF", Mbf)); + + foreach (var v in variables) { + if (v.Value is null || v.Key == "A" || v.Key == "FP") + continue; + paramArr.Add(NetworkArray.Param(v.Key, v.Value)); + } + } + + arr.Add(paramArr); + return arr; + } + + private void addVariableToArray(NetworkArray paramArr, string varName) { + if (variables.TryGetValue (varName, out string tmp) && tmp != null) { + paramArr.Add(NetworkArray.Param(varName, tmp)); + } + } + + private string FixMountState(string value) { + // raised pet geometry - set from Fp + PetGeometryType GeometryType = PetGeometryType.Default; + // raised pet age - set from Fp + PetAge PetAge = PetAge.Adult; + // raised pet mounted - set from Fp + bool PetMounted = false; + + string[] array = value.Split('*'); + Dictionary keyValPairs = new(); + foreach (string str in array) { + string[] keyValPair = str.Split('$'); + if (keyValPair.Length == 2) + keyValPairs[keyValPair[0]] = keyValPair[1]; + } + if (keyValPairs.TryGetValue("G", out string geometry)) + if (geometry.ToLower().Contains("terribleterror")) + GeometryType = PetGeometryType.Terror; + if (keyValPairs.TryGetValue("A", out string age)) { + switch (age) { + case "E": PetAge = PetAge.EggInHand; break; + case "B": PetAge = PetAge.Baby; break; + case "C": PetAge = PetAge.Child; break; + case "T": PetAge = PetAge.Teen; break; + case "A": PetAge = PetAge.Adult; break; + case "Ti": PetAge = PetAge.Titan; break; + } + } + if (keyValPairs.TryGetValue("U", out string userdata)) { + PetMounted = (userdata == "0" || userdata == "1"); + } + if (PetMounted && !Configuration.ServerConfiguration.AllowChaos && + (GeometryType == PetGeometryType.Default && PetAge < PetAge.Teen + || GeometryType == PetGeometryType.Terror && PetAge < PetAge.Titan) + ) { + return Regex.Replace(value, "^U\\$[01]\\*", "U$-1*"); + } else { + return value; + } + } +} + +public enum PetGeometryType { + Default, + Terror +} +public enum PetAge { + EggInHand = 0, + Baby = 1, + Child = 2, + Teen = 3, + Adult = 4, + Titan = 5 +} diff --git a/src/Data/SocketBuffer.cs b/src/Data/SocketBuffer.cs new file mode 100644 index 0000000..1dfa6e2 --- /dev/null +++ b/src/Data/SocketBuffer.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace sodoffmmo.Data; +internal class SocketBuffer { + NetworkData data = new(); + Status status = Status.Header; + byte header; + short length; + byte[] value = new byte[0]; + public void Write(byte[] buffer, int length) { + data.WriteChunk(buffer, 0, length); + } + + public bool ReadPacket(out NetworkPacket packet) { + packet = new(); + if (status == Status.Header) + ReadHeader(); + + if (status == Status.Value) { + ReadValue(); + if (status == Status.Header) { + packet = new(header, value); + return true; + } + } + + return false; + } + + private void ReadHeader() { + if (data.RemainingLength < 3) + return; + header = data.ReadByte(); + length = data.ReadShort(); + status = Status.Value; + } + + private void ReadValue() { + if (data.RemainingLength < length) + return; + value = data.ReadChunk(length); + data = new(data.ReadChunk(data.RemainingLength)); + status = Status.Header; + } + + enum Status { + Header, + Value + } +} diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..99edc06 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,18 @@ +# Use the official .NET SDK image for building the application +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Copy the source code +COPY . . + +# Restore dependencies and build the application +RUN dotnet build -c Release -o /app + +# Create clean run environment (without source and sdk) +# FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base +# WORKDIR /app +# COPY --from=build /app . + +# Run the application +WORKDIR /app +ENTRYPOINT ["./sodoffmmo"] diff --git a/src/Management/AuthenticationInfo.cs b/src/Management/AuthenticationInfo.cs new file mode 100644 index 0000000..266ebc1 --- /dev/null +++ b/src/Management/AuthenticationInfo.cs @@ -0,0 +1,20 @@ +using System.Xml.Serialization; + +namespace sodoffmmo.Management; + +[Serializable] +public class AuthenticationInfo { + [XmlElement] + public bool Authenticated { get; set; } + + [XmlElement] + public string DisplayName { get; set; } + + [XmlElement] + public Role Role { get; set; } +} + +[Serializable] +public enum Role { + User = 0, Moderator = 1, Admin = 2 +} \ No newline at end of file diff --git a/src/Management/Commands/AnnounceCommand.cs b/src/Management/Commands/AnnounceCommand.cs new file mode 100644 index 0000000..3f4a2c7 --- /dev/null +++ b/src/Management/Commands/AnnounceCommand.cs @@ -0,0 +1,15 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("announce", Role.Admin)] +class AnnounceCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + if (arguments.Length == 0) { + client.Send(Utils.BuildServerSideMessage("Announce: No message to announce", "Server")); + return; + } + client.Room.Send(Utils.BuildServerSideMessage(string.Join(' ', arguments), "Server")); + } +} diff --git a/src/Management/Commands/BypassCommand.cs b/src/Management/Commands/BypassCommand.cs new file mode 100644 index 0000000..dc2d644 --- /dev/null +++ b/src/Management/Commands/BypassCommand.cs @@ -0,0 +1,27 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("bypass", Role.Admin)] +class BypassCommand : IManagementCommand +{ + public void Handle(Client client, string[] arguments) { + if (arguments.Length == 0) { + client.Send(Utils.BuildServerSideMessage("Bypass: No message to send", "Server")); + return; + } + + string message = string.Join(' ', arguments); + client.Room.Send(Utils.BuildChatMessage(client.PlayerData.Uid, message, client.PlayerData.DiplayName), client); + + NetworkObject cmd = new(); + NetworkObject data = new(); + data.Add("arr", new string[] { "SCA", "-1", "1", message, "", "1" }); + cmd.Add("c", "SCA"); + cmd.Add("p", data); + NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); + client.Send(packet); + } +} diff --git a/src/Management/Commands/DisableAllChatsCommand.cs b/src/Management/Commands/DisableAllChatsCommand.cs new file mode 100644 index 0000000..d457b4c --- /dev/null +++ b/src/Management/Commands/DisableAllChatsCommand.cs @@ -0,0 +1,13 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("disableallchats", Role.Moderator)] +class DisableAllChatsCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + Room.DisableAllChatOverrides(); + client.Room.Send(Utils.BuildServerSideMessage("All chat overrides have been disabled", "Server")); + } +} + diff --git a/src/Management/Commands/DisableChatCommand.cs b/src/Management/Commands/DisableChatCommand.cs new file mode 100644 index 0000000..7c8ab5f --- /dev/null +++ b/src/Management/Commands/DisableChatCommand.cs @@ -0,0 +1,12 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("disablechat", Role.Moderator)] +class DisableChatCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + client.Room.AllowChatOverride = false; + client.Room.Send(Utils.BuildServerSideMessage("Chat has been disabled", "Server")); + } +} diff --git a/src/Management/Commands/EnableChatCommand.cs b/src/Management/Commands/EnableChatCommand.cs new file mode 100644 index 0000000..8624c60 --- /dev/null +++ b/src/Management/Commands/EnableChatCommand.cs @@ -0,0 +1,12 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("enablechat", Role.Moderator)] +class EnableChatCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + client.Room.AllowChatOverride = true; + client.Room.Send(Utils.BuildServerSideMessage("Chat has been enabled", "Server")); + } +} diff --git a/src/Management/Commands/ListAllChatOverridesCommand.cs b/src/Management/Commands/ListAllChatOverridesCommand.cs new file mode 100644 index 0000000..3032a23 --- /dev/null +++ b/src/Management/Commands/ListAllChatOverridesCommand.cs @@ -0,0 +1,12 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("listallchats", Role.Moderator)] +class ListAllChatOverridesCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + client.Send(Utils.BuildServerSideMessage(string.Join(' ', Room.AllRooms().Where(x => x.AllowChatOverride).Select(x => x.Name)), "Server")); + } +} + diff --git a/src/Management/Commands/PlayerCount.cs b/src/Management/Commands/PlayerCount.cs new file mode 100644 index 0000000..5f73c2d --- /dev/null +++ b/src/Management/Commands/PlayerCount.cs @@ -0,0 +1,16 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("playercount", Role.Admin)] +class PlayerCount : IManagementCommand { + public void Handle(Client client, string[] arguments) { + client.Send(Utils.BuildServerSideMessage($"Current room: {(client.Room?.ClientsCount.ToString() ?? "not in room")}, Server total: {Room.AllRooms().Sum(x => x.ClientsCount)}", "Server")); + } +} diff --git a/src/Management/Commands/TempMuteCommand.cs b/src/Management/Commands/TempMuteCommand.cs new file mode 100644 index 0000000..6479173 --- /dev/null +++ b/src/Management/Commands/TempMuteCommand.cs @@ -0,0 +1,24 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("tempmute", Role.Moderator)] +class TempMuteCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + if (arguments.Length != 1) { + client.Send(Utils.BuildServerSideMessage("TempMute: Invalid number of arguments", "Server")); + return; + } + Client? target = client.Room.Clients.FirstOrDefault(x => x.PlayerData.DiplayName == arguments[0]); + if (target == null) { + client.Send(Utils.BuildServerSideMessage($"TempMute: user {arguments[0]} not found", "Server")); + return; + } + target.TempMuted = !target.TempMuted; + if (target.TempMuted) + client.Send(Utils.BuildServerSideMessage($"TempMute: {arguments[0]} has been temporarily muted", "Server")); + else + client.Send(Utils.BuildServerSideMessage($"TempMute: {arguments[0]} has been unmuted", "Server")); + } +} diff --git a/src/Management/IManagementCommand.cs b/src/Management/IManagementCommand.cs new file mode 100644 index 0000000..3fcd8c3 --- /dev/null +++ b/src/Management/IManagementCommand.cs @@ -0,0 +1,11 @@ +using sodoffmmo.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace sodoffmmo.Management; +public interface IManagementCommand { + public void Handle(Client client, string[] arguments); +} diff --git a/src/Management/ManagementCommandProcessor.cs b/src/Management/ManagementCommandProcessor.cs new file mode 100644 index 0000000..c4b35eb --- /dev/null +++ b/src/Management/ManagementCommandProcessor.cs @@ -0,0 +1,52 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace sodoffmmo.Management; +public class ManagementCommandProcessor { + static Dictionary, Type> commands = new(); + static bool initialized = false; + + public static void Initialize() { + if (Configuration.ServerConfiguration.Authentication == AuthenticationMode.Disabled) + return; + commands.Clear(); + var handlerTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(type => typeof(IManagementCommand).IsAssignableFrom(type)) + .Where(type => type.GetCustomAttribute() != null); + + foreach (var handlerType in handlerTypes) { + ManagementCommandAttribute attrib = (handlerType.GetCustomAttribute(typeof(ManagementCommandAttribute)) as ManagementCommandAttribute)!; + commands[new Tuple(attrib.Name, attrib.Role)] = handlerType; + } + initialized = true; + } + + public static bool ProcessCommand(string message, Client client) { + if (!initialized) + return false; + if (!message.StartsWith("::") || message.Length < 3) + return false; + + string[] parts = message.Split(' '); + string commandName = parts[0].Substring(2); + string[] arguments = parts.Skip(1).ToArray(); + + for (int i = (int)client.PlayerData.Role; i >= 0; --i) { + Role currentRole = (Role)i; + + if (commands.TryGetValue(new Tuple(commandName, currentRole), out Type? commandType)) { + IManagementCommand command = (IManagementCommand)Activator.CreateInstance(commandType)!; + Console.WriteLine($"Management command {commandName} by {client.PlayerData.DiplayName} ({client.PlayerData.Uid}) in {client.Room.Name}"); + command.Handle(client, arguments); + return true; + } + } + return false; + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..6878f22 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,22 @@ +using sodoffmmo; +using sodoffmmo.Core; +using System.Net; + +Configuration.Initialize(); + +Server server; + +if (String.IsNullOrEmpty(Configuration.ServerConfiguration.ListenIP) || Configuration.ServerConfiguration.ListenIP == "*") { + server = new( + IPAddress.IPv6Any, + Configuration.ServerConfiguration.Port, + true + ); +} else { + server = new( + IPAddress.Parse(Configuration.ServerConfiguration.ListenIP), + Configuration.ServerConfiguration.Port, + false + ); +} +await server.Run(); diff --git a/src/Server.cs b/src/Server.cs new file mode 100644 index 0000000..77f78df --- /dev/null +++ b/src/Server.cs @@ -0,0 +1,165 @@ +using sodoffmmo.Core; +using sodoffmmo.Data; +using sodoffmmo.Management; +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace sodoffmmo; +public class Server { + + readonly int port; + readonly IPAddress ipAddress; + readonly bool IPv6AndIPv4; + ModuleManager moduleManager = new(); + + public static List? AllClients { get; private set; } + public static Dictionary? UDPClients { get; private set; } + public static UdpClient SharedUDPClient { get; private set; } + + public Server(IPAddress ipAdress, int port, bool IPv6AndIPv4) { + this.ipAddress = ipAdress; + this.port = port; + this.IPv6AndIPv4 = IPv6AndIPv4; + + AllClients = []; + UDPClients = []; + } + + public async Task Run() { + moduleManager.RegisterModules(); + ManagementCommandProcessor.Initialize(); + using Socket listener = new(ipAddress.AddressFamily, + SocketType.Stream, + ProtocolType.Tcp); + if (IPv6AndIPv4) + listener.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0); + listener.Bind(new IPEndPoint(ipAddress, port)); + + SpecialRoom.CreateRooms(); + + Task tcpTask = Listen(listener); + + using var cts = new CancellationTokenSource(); + Task udpTask = ListenAndHandleUDP(cts.Token); + + try { await Task.WhenAll(tcpTask, udpTask); } catch (OperationCanceledException) { Console.WriteLine($"UDP Listener Exited Gracefully"); } + catch (Exception ex) { Console.WriteLine(ex.ToString()); } + } + + private async Task Listen(Socket listener) { + Console.WriteLine($"MMO Server listening on port {port}"); + listener.Listen(100); + while (true) { + Socket handler = await listener.AcceptAsync(); + handler.SendTimeout = 200; + Console.WriteLine($"New connection from {((IPEndPoint)handler.RemoteEndPoint!).Address}"); + _ = Task.Run(() => HandleClient(handler)); + } + } + + private async Task ListenAndHandleUDP(CancellationToken cancellationToken) + { + using var udpClient = new UdpClient(port); + SharedUDPClient = udpClient; // this should get disposed when the server shuts down (maybe) + + Console.WriteLine($"MMO UDP Listener listening on port {port}"); + + while (!cancellationToken.IsCancellationRequested) + { + UdpReceiveResult result = await udpClient.ReceiveAsync(cancellationToken); + byte[] buffer = result.Buffer; + IPEndPoint endPoint = result.RemoteEndPoint; + + NetworkObject obj; + try { obj = new(buffer[3..]); } + catch { continue; } + + if(obj.ContainsKey("h")) // init + { + int tcpClientId = obj.Get("u"); + Client? tcpClient = AllClients?.FirstOrDefault(e => e.ClientID == tcpClientId); + if (tcpClient == null) continue; + + Console.WriteLine($"Received UDP handshake attempt from client with ID {tcpClient.ClientID}"); + + NetworkObject ackObj = new(); + ackObj.Add("h", (byte)1); + + UDPClients?[endPoint] = tcpClient; + tcpClient.UDPEndPoint = endPoint; + + NetworkPacket ack = ackObj.Serialize(); + await SharedUDPClient.SendAsync(ack.SendData, ack.SendData.Length, endPoint); // for ACK, the same UdpClient instance needs to respond so the client doesn't keep pinging + continue; + } + + if (!UDPClients!.TryGetValue(endPoint, out Client? client)) + continue; + + await HandleObjectsUDP([obj], client); + } + } + + private async Task HandleClient(Socket handler) { + Client client = new(handler); + AllClients?.Add(client); + try { + while (client.Connected) { + await client.Receive(); + List networkObjects = new(); + while (client.TryGetNextPacket(out NetworkPacket packet)) + networkObjects.Add(packet.GetObject()); + + await HandleObjects(networkObjects, client); + } + } finally { + try { + client.SetRoom(null); + AllClients?.Remove(client); + UDPClients?.Remove(client.UDPEndPoint); + } catch (Exception) { } + client.Disconnect(); + Console.WriteLine("Socket disconnected IID: " + client.ClientID); + } + } + + private async Task HandleObjects(List networkObjects, Client client) { + foreach (var obj in networkObjects) { + try { + short commandId = obj.Get("a"); + CommandHandler handler; + if (commandId != 13) { + if (commandId == 0 || commandId == 1) + Console.WriteLine($"System command: {commandId} IID: {client.ClientID}"); + handler = moduleManager.GetCommandHandler(commandId); + } else + handler = moduleManager.GetCommandHandler(obj.Get("p").Get("c")); + Task task = handler.Handle(client, obj.Get("p")); + if (!handler.RunInBackground) + await task; + } catch (Exception ex) { + Console.WriteLine($"Exception IID: {client.ClientID} - {ex}"); + } + } + } + + private async Task HandleObjectsUDP(List networkObjects, Client client) + { + foreach (var obj in networkObjects) + { + try + { + // only handle extension commands for now as all system commands (other than UDP init) should be handled by the TCP connection + CommandHandler handler = moduleManager.GetCommandHandler(obj.Get("p").Get("c")); + Task task = handler.Handle(client, obj.Get("p")); + if (!handler.RunInBackground) + await task; + } catch (Exception ex) + { + Console.WriteLine($"Exception IID: {client.ClientID} - {ex}"); + } + } + } +} diff --git a/src/appsettings.json b/src/appsettings.json new file mode 100644 index 0000000..8274697 --- /dev/null +++ b/src/appsettings.json @@ -0,0 +1,73 @@ +{ + "MMOServer": { + "// ListenIP": "Listening IP address for the MMO server, default is '*' which represents all IPv4 and IPv6 addresses", + "ListenIP": "*", + + "// Port": "Listening port number for the MMO server", + "Port": 9933, + + "// PingDelay": "delay (in milliseconds) for PNG response", + "PingDelay": 17, + + "// EnableChat": "When true, in-game chat will be enabled", + "EnableChat": true, + "EnableCannedChat": true, + + "// EventName": "World event name send to client (can be used to select ship type after modding WorldEventScoutAttack in client)", + "EventName": "ScoutAttack", + + "// FirstEventTimer": "time to start of first world event (battle ship event) after start MMO server", + "FirstEventTimer": 3, + + "// EventTimer": "time between start of world events (battle ship events), set both timer values (EventTimer and FirstEventTimer) to 0 to disable events", + "EventTimer": 30, + + "// RoomAlerts": "List of MMO rooms with alert function. Default empty (not used by SoD), bellow sample config for WoJS, MB and SS.", + "// alert parameters": "alert type, duration [s], minimum time to start [s], maximum time to start [s], redAlertDuration (used for type '1'), number of songs (used for type '3')", + "// alert types": "1 - Red Alert, 2 - Disco Alert, 3 - Dance Off", + "RoomAlerts": { + "LoungeInt" : [ ["3", 20.0, 30, 240, 0, 16] ], + "Spaceport": [ ["1", 20.0, 300, 300, 60, 0], ["2", 120.0, 1800, 3600, 60, 0] ], + "Academy": [ ["1", 20.0, 300, 300, 60, 0] ], + "ClubSSInt" : [ ["3", 20.0, 30, 240, 0, 16] ], + "JunkYardEMD": [ ["1", 20.0, 240, 300, 60, 0] ], + }, + + "// AmbassadorRooms": "Rooms with ambassadors (MB funzones).", + "AmbassadorRooms": ["Spaceport"], + + "// AmbassadorGaugeStart": "The starting value for all ambassador gauges (MB funzones).", + "AmbassadorGaugeStart": 75, + + "// AmbassadorGaugeDecayRate": "Time in seconds before ambassador gauges decrease (MB funzones).", + "AmbassadorGaugeDecayRate": 60, + + "// AmbassadorGaugeDecayOnlyWhenInRoom": "Only decrease ambassador gauges when there is at least one player in the room (MB funzones).", + "AmbassadorGaugeDecayOnlyWhenInRoom": true, + + "// AmbassadorGaugePlayers": "Denominator for filling the ambassador gauges (MB funzones).", + "AmbassadorGaugePlayers": 0.5, + + "// RacingMaxPlayers": "maximum players allowed in Thunder Run Racing (no more than 6)", + "RacingMaxPlayers": 6, + + "// RacingMinPlayers": "minimum players to start Thunder Run Racing", + "RacingMinPlayers": 2, + + "// AllowChaos": "disable server side exploit protection", + "AllowChaos": false, + + "// Authentication": "Player authentication mode: Disabled, Optional, RequiredForChat, Required", + "// Authentication Disabled": "authentication is disabled, anyone can connect to mmo", + "// Authentication Optional": "authentication is required only for moderation activities", + "// Authentication RequiredForChat": "authentication is required only for moderation activities and using chat (if chat is enabled)", + "// Authentication Required": "authentication is required to connect to mmo", + "Authentication": "Disabled", + + "// ApiUrl": "SoDOff API server URL for authentication calls", + "ApiUrl": "http://localhost:5000", + + "// BypassToken": "Token allowed to connect without authentication", + "BypassToken": "" + } +} diff --git a/src/sodoffmmo.csproj b/src/sodoffmmo.csproj new file mode 100644 index 0000000..74b566a --- /dev/null +++ b/src/sodoffmmo.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + PreserveNewest + + +