Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e3efeda04 | ||
|
|
23df0c7123 | ||
|
|
ee76b39d71 | ||
|
|
c25bfd92f6 | ||
|
|
1e974dcb7d | ||
|
|
79de053eb0 | ||
|
|
26045d132f | ||
|
|
e4d5c8587d | ||
|
|
b6d9c0787d | ||
|
|
3101cf89cc | ||
|
|
a96ec01575 | ||
|
|
61add85e86 | ||
|
|
6bca75b3ac | ||
|
|
7d9aba43ec | ||
|
|
ff1ce90d16 | ||
|
|
cbb5f65f1d | ||
|
|
b19ced54c0 | ||
|
|
19e3a6cbdb | ||
|
|
ed3b9594e8 |
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
[submodule "Velocity"]
|
|
||||||
path = Velocity
|
|
||||||
url = https://github.com/PaperMC/Velocity
|
|
||||||
504
LICENSE
504
LICENSE
|
|
@ -1,504 +0,0 @@
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
|
||||||
Version 2.1, February 1999
|
|
||||||
|
|
||||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
[This is the first released version of the Lesser GPL. It also counts
|
|
||||||
as the successor of the GNU Library Public License, version 2, hence
|
|
||||||
the version number 2.1.]
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
Licenses are intended to guarantee your freedom to share and change
|
|
||||||
free software--to make sure the software is free for all its users.
|
|
||||||
|
|
||||||
This license, the Lesser General Public License, applies to some
|
|
||||||
specially designated software packages--typically libraries--of the
|
|
||||||
Free Software Foundation and other authors who decide to use it. You
|
|
||||||
can use it too, but we suggest you first think carefully about whether
|
|
||||||
this license or the ordinary General Public License is the better
|
|
||||||
strategy to use in any particular case, based on the explanations below.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom of use,
|
|
||||||
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 this service if you wish); that you receive source code or can get
|
|
||||||
it if you want it; that you can change the software and use pieces of
|
|
||||||
it in new free programs; and that you are informed that you can do
|
|
||||||
these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
distributors to deny you these rights or to ask you to surrender these
|
|
||||||
rights. These restrictions translate to certain responsibilities for
|
|
||||||
you if you distribute copies of the library or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of the library, whether gratis
|
|
||||||
or for a fee, you must give the recipients all the rights that we gave
|
|
||||||
you. You must make sure that they, too, receive or can get the source
|
|
||||||
code. If you link other code with the library, you must provide
|
|
||||||
complete object files to the recipients, so that they can relink them
|
|
||||||
with the library after making changes to the library and recompiling
|
|
||||||
it. And you must show them these terms so they know their rights.
|
|
||||||
|
|
||||||
We protect your rights with a two-step method: (1) we copyright the
|
|
||||||
library, and (2) we offer you this license, which gives you legal
|
|
||||||
permission to copy, distribute and/or modify the library.
|
|
||||||
|
|
||||||
To protect each distributor, we want to make it very clear that
|
|
||||||
there is no warranty for the free library. Also, if the library is
|
|
||||||
modified by someone else and passed on, the recipients should know
|
|
||||||
that what they have is not the original version, so that the original
|
|
||||||
author's reputation will not be affected by problems that might be
|
|
||||||
introduced by others.
|
|
||||||
|
|
||||||
Finally, software patents pose a constant threat to the existence of
|
|
||||||
any free program. We wish to make sure that a company cannot
|
|
||||||
effectively restrict the users of a free program by obtaining a
|
|
||||||
restrictive license from a patent holder. Therefore, we insist that
|
|
||||||
any patent license obtained for a version of the library must be
|
|
||||||
consistent with the full freedom of use specified in this license.
|
|
||||||
|
|
||||||
Most GNU software, including some libraries, is covered by the
|
|
||||||
ordinary GNU General Public License. This license, the GNU Lesser
|
|
||||||
General Public License, applies to certain designated libraries, and
|
|
||||||
is quite different from the ordinary General Public License. We use
|
|
||||||
this license for certain libraries in order to permit linking those
|
|
||||||
libraries into non-free programs.
|
|
||||||
|
|
||||||
When a program is linked with a library, whether statically or using
|
|
||||||
a shared library, the combination of the two is legally speaking a
|
|
||||||
combined work, a derivative of the original library. The ordinary
|
|
||||||
General Public License therefore permits such linking only if the
|
|
||||||
entire combination fits its criteria of freedom. The Lesser General
|
|
||||||
Public License permits more lax criteria for linking other code with
|
|
||||||
the library.
|
|
||||||
|
|
||||||
We call this license the "Lesser" General Public License because it
|
|
||||||
does Less to protect the user's freedom than the ordinary General
|
|
||||||
Public License. It also provides other free software developers Less
|
|
||||||
of an advantage over competing non-free programs. These disadvantages
|
|
||||||
are the reason we use the ordinary General Public License for many
|
|
||||||
libraries. However, the Lesser license provides advantages in certain
|
|
||||||
special circumstances.
|
|
||||||
|
|
||||||
For example, on rare occasions, there may be a special need to
|
|
||||||
encourage the widest possible use of a certain library, so that it becomes
|
|
||||||
a de-facto standard. To achieve this, non-free programs must be
|
|
||||||
allowed to use the library. A more frequent case is that a free
|
|
||||||
library does the same job as widely used non-free libraries. In this
|
|
||||||
case, there is little to gain by limiting the free library to free
|
|
||||||
software only, so we use the Lesser General Public License.
|
|
||||||
|
|
||||||
In other cases, permission to use a particular library in non-free
|
|
||||||
programs enables a greater number of people to use a large body of
|
|
||||||
free software. For example, permission to use the GNU C Library in
|
|
||||||
non-free programs enables many more people to use the whole GNU
|
|
||||||
operating system, as well as its variant, the GNU/Linux operating
|
|
||||||
system.
|
|
||||||
|
|
||||||
Although the Lesser General Public License is Less protective of the
|
|
||||||
users' freedom, it does ensure that the user of a program that is
|
|
||||||
linked with the Library has the freedom and the wherewithal to run
|
|
||||||
that program using a modified version of the Library.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow. Pay close attention to the difference between a
|
|
||||||
"work based on the library" and a "work that uses the library". The
|
|
||||||
former contains code derived from the library, whereas the latter must
|
|
||||||
be combined with the library in order to run.
|
|
||||||
|
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License Agreement applies to any software library or other
|
|
||||||
program which contains a notice placed by the copyright holder or
|
|
||||||
other authorized party saying it may be distributed under the terms of
|
|
||||||
this Lesser General Public License (also called "this License").
|
|
||||||
Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
A "library" means a collection of software functions and/or data
|
|
||||||
prepared so as to be conveniently linked with application programs
|
|
||||||
(which use some of those functions and data) to form executables.
|
|
||||||
|
|
||||||
The "Library", below, refers to any such software library or work
|
|
||||||
which has been distributed under these terms. A "work based on the
|
|
||||||
Library" means either the Library or any derivative work under
|
|
||||||
copyright law: that is to say, a work containing the Library or a
|
|
||||||
portion of it, either verbatim or with modifications and/or translated
|
|
||||||
straightforwardly into another language. (Hereinafter, translation is
|
|
||||||
included without limitation in the term "modification".)
|
|
||||||
|
|
||||||
"Source code" for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For a library, complete source code means
|
|
||||||
all the source code for all modules it contains, plus any associated
|
|
||||||
interface definition files, plus the scripts used to control compilation
|
|
||||||
and installation of the library.
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running a program using the Library is not restricted, and output from
|
|
||||||
such a program is covered only if its contents constitute a work based
|
|
||||||
on the Library (independent of the use of the Library in a tool for
|
|
||||||
writing it). Whether that is true depends on what the Library does
|
|
||||||
and what the program that uses the Library does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Library's
|
|
||||||
complete source code as you receive it, in any medium, provided that
|
|
||||||
you conspicuously and appropriately publish on each copy an
|
|
||||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
|
||||||
all the notices that refer to this License and to the absence of any
|
|
||||||
warranty; and distribute a copy of this License along with the
|
|
||||||
Library.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy,
|
|
||||||
and you may at your option offer warranty protection in exchange for a
|
|
||||||
fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Library or any portion
|
|
||||||
of it, thus forming a work based on the Library, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The modified work must itself be a software library.
|
|
||||||
|
|
||||||
b) You must cause the files modified to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
c) You must cause the whole of the work to be licensed at no
|
|
||||||
charge to all third parties under the terms of this License.
|
|
||||||
|
|
||||||
d) If a facility in the modified Library refers to a function or a
|
|
||||||
table of data to be supplied by an application program that uses
|
|
||||||
the facility, other than as an argument passed when the facility
|
|
||||||
is invoked, then you must make a good faith effort to ensure that,
|
|
||||||
in the event an application does not supply such function or
|
|
||||||
table, the facility still operates, and performs whatever part of
|
|
||||||
its purpose remains meaningful.
|
|
||||||
|
|
||||||
(For example, a function in a library to compute square roots has
|
|
||||||
a purpose that is entirely well-defined independent of the
|
|
||||||
application. Therefore, Subsection 2d requires that any
|
|
||||||
application-supplied function or table used by this function must
|
|
||||||
be optional: if the application does not supply it, the square
|
|
||||||
root function must still compute square roots.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Library,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Library, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote
|
|
||||||
it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Library.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Library
|
|
||||||
with the Library (or with a work based on the Library) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
|
||||||
License instead of this License to a given copy of the Library. To do
|
|
||||||
this, you must alter all the notices that refer to this License, so
|
|
||||||
that they refer to the ordinary GNU General Public License, version 2,
|
|
||||||
instead of to this License. (If a newer version than version 2 of the
|
|
||||||
ordinary GNU General Public License has appeared, then you can specify
|
|
||||||
that version instead if you wish.) Do not make any other change in
|
|
||||||
these notices.
|
|
||||||
|
|
||||||
Once this change is made in a given copy, it is irreversible for
|
|
||||||
that copy, so the ordinary GNU General Public License applies to all
|
|
||||||
subsequent copies and derivative works made from that copy.
|
|
||||||
|
|
||||||
This option is useful when you wish to copy part of the code of
|
|
||||||
the Library into a program that is not a library.
|
|
||||||
|
|
||||||
4. You may copy and distribute the Library (or a portion or
|
|
||||||
derivative of it, under Section 2) in object code or executable form
|
|
||||||
under the terms of Sections 1 and 2 above provided that you accompany
|
|
||||||
it with the complete corresponding machine-readable source code, which
|
|
||||||
must be distributed under the terms of Sections 1 and 2 above on a
|
|
||||||
medium customarily used for software interchange.
|
|
||||||
|
|
||||||
If distribution of object code is made by offering access to copy
|
|
||||||
from a designated place, then offering equivalent access to copy the
|
|
||||||
source code from the same place satisfies the requirement to
|
|
||||||
distribute the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
5. A program that contains no derivative of any portion of the
|
|
||||||
Library, but is designed to work with the Library by being compiled or
|
|
||||||
linked with it, is called a "work that uses the Library". Such a
|
|
||||||
work, in isolation, is not a derivative work of the Library, and
|
|
||||||
therefore falls outside the scope of this License.
|
|
||||||
|
|
||||||
However, linking a "work that uses the Library" with the Library
|
|
||||||
creates an executable that is a derivative of the Library (because it
|
|
||||||
contains portions of the Library), rather than a "work that uses the
|
|
||||||
library". The executable is therefore covered by this License.
|
|
||||||
Section 6 states terms for distribution of such executables.
|
|
||||||
|
|
||||||
When a "work that uses the Library" uses material from a header file
|
|
||||||
that is part of the Library, the object code for the work may be a
|
|
||||||
derivative work of the Library even though the source code is not.
|
|
||||||
Whether this is true is especially significant if the work can be
|
|
||||||
linked without the Library, or if the work is itself a library. The
|
|
||||||
threshold for this to be true is not precisely defined by law.
|
|
||||||
|
|
||||||
If such an object file uses only numerical parameters, data
|
|
||||||
structure layouts and accessors, and small macros and small inline
|
|
||||||
functions (ten lines or less in length), then the use of the object
|
|
||||||
file is unrestricted, regardless of whether it is legally a derivative
|
|
||||||
work. (Executables containing this object code plus portions of the
|
|
||||||
Library will still fall under Section 6.)
|
|
||||||
|
|
||||||
Otherwise, if the work is a derivative of the Library, you may
|
|
||||||
distribute the object code for the work under the terms of Section 6.
|
|
||||||
Any executables containing that work also fall under Section 6,
|
|
||||||
whether or not they are linked directly with the Library itself.
|
|
||||||
|
|
||||||
6. As an exception to the Sections above, you may also combine or
|
|
||||||
link a "work that uses the Library" with the Library to produce a
|
|
||||||
work containing portions of the Library, and distribute that work
|
|
||||||
under terms of your choice, provided that the terms permit
|
|
||||||
modification of the work for the customer's own use and reverse
|
|
||||||
engineering for debugging such modifications.
|
|
||||||
|
|
||||||
You must give prominent notice with each copy of the work that the
|
|
||||||
Library is used in it and that the Library and its use are covered by
|
|
||||||
this License. You must supply a copy of this License. If the work
|
|
||||||
during execution displays copyright notices, you must include the
|
|
||||||
copyright notice for the Library among them, as well as a reference
|
|
||||||
directing the user to the copy of this License. Also, you must do one
|
|
||||||
of these things:
|
|
||||||
|
|
||||||
a) Accompany the work with the complete corresponding
|
|
||||||
machine-readable source code for the Library including whatever
|
|
||||||
changes were used in the work (which must be distributed under
|
|
||||||
Sections 1 and 2 above); and, if the work is an executable linked
|
|
||||||
with the Library, with the complete machine-readable "work that
|
|
||||||
uses the Library", as object code and/or source code, so that the
|
|
||||||
user can modify the Library and then relink to produce a modified
|
|
||||||
executable containing the modified Library. (It is understood
|
|
||||||
that the user who changes the contents of definitions files in the
|
|
||||||
Library will not necessarily be able to recompile the application
|
|
||||||
to use the modified definitions.)
|
|
||||||
|
|
||||||
b) Use a suitable shared library mechanism for linking with the
|
|
||||||
Library. A suitable mechanism is one that (1) uses at run time a
|
|
||||||
copy of the library already present on the user's computer system,
|
|
||||||
rather than copying library functions into the executable, and (2)
|
|
||||||
will operate properly with a modified version of the library, if
|
|
||||||
the user installs one, as long as the modified version is
|
|
||||||
interface-compatible with the version that the work was made with.
|
|
||||||
|
|
||||||
c) Accompany the work with a written offer, valid for at
|
|
||||||
least three years, to give the same user the materials
|
|
||||||
specified in Subsection 6a, above, for a charge no more
|
|
||||||
than the cost of performing this distribution.
|
|
||||||
|
|
||||||
d) If distribution of the work is made by offering access to copy
|
|
||||||
from a designated place, offer equivalent access to copy the above
|
|
||||||
specified materials from the same place.
|
|
||||||
|
|
||||||
e) Verify that the user has already received a copy of these
|
|
||||||
materials or that you have already sent this user a copy.
|
|
||||||
|
|
||||||
For an executable, the required form of the "work that uses the
|
|
||||||
Library" must include any data and utility programs needed for
|
|
||||||
reproducing the executable from it. However, as a special exception,
|
|
||||||
the materials to be distributed need not include anything that is
|
|
||||||
normally distributed (in either source or binary form) with the major
|
|
||||||
components (compiler, kernel, and so on) of the operating system on
|
|
||||||
which the executable runs, unless that component itself accompanies
|
|
||||||
the executable.
|
|
||||||
|
|
||||||
It may happen that this requirement contradicts the license
|
|
||||||
restrictions of other proprietary libraries that do not normally
|
|
||||||
accompany the operating system. Such a contradiction means you cannot
|
|
||||||
use both them and the Library together in an executable that you
|
|
||||||
distribute.
|
|
||||||
|
|
||||||
7. You may place library facilities that are a work based on the
|
|
||||||
Library side-by-side in a single library together with other library
|
|
||||||
facilities not covered by this License, and distribute such a combined
|
|
||||||
library, provided that the separate distribution of the work based on
|
|
||||||
the Library and of the other library facilities is otherwise
|
|
||||||
permitted, and provided that you do these two things:
|
|
||||||
|
|
||||||
a) Accompany the combined library with a copy of the same work
|
|
||||||
based on the Library, uncombined with any other library
|
|
||||||
facilities. This must be distributed under the terms of the
|
|
||||||
Sections above.
|
|
||||||
|
|
||||||
b) Give prominent notice with the combined library of the fact
|
|
||||||
that part of it is a work based on the Library, and explaining
|
|
||||||
where to find the accompanying uncombined form of the same work.
|
|
||||||
|
|
||||||
8. You may not copy, modify, sublicense, link with, or distribute
|
|
||||||
the Library except as expressly provided under this License. Any
|
|
||||||
attempt otherwise to copy, modify, sublicense, link with, or
|
|
||||||
distribute the Library is void, and will automatically terminate your
|
|
||||||
rights under this License. However, parties who have received copies,
|
|
||||||
or rights, from you under this License will not have their licenses
|
|
||||||
terminated so long as such parties remain in full compliance.
|
|
||||||
|
|
||||||
9. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Library or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Library (or any work based on the
|
|
||||||
Library), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Library or works based on it.
|
|
||||||
|
|
||||||
10. Each time you redistribute the Library (or any work based on the
|
|
||||||
Library), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute, link with or modify the Library
|
|
||||||
subject to these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties with
|
|
||||||
this License.
|
|
||||||
|
|
||||||
11. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
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
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Library at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Library by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Library.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under any
|
|
||||||
particular circumstance, the balance of the section is intended to apply,
|
|
||||||
and the section as a whole is intended to apply in other circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
12. If the distribution and/or use of the Library is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Library under this License may add
|
|
||||||
an explicit geographical distribution limitation excluding those countries,
|
|
||||||
so that distribution is permitted only in or among countries not thus
|
|
||||||
excluded. In such case, this License incorporates the limitation as if
|
|
||||||
written in the body of this License.
|
|
||||||
|
|
||||||
13. The Free Software Foundation may publish revised and/or new
|
|
||||||
versions of the Lesser 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 Library
|
|
||||||
specifies a version number of this License which applies to it and
|
|
||||||
"any later version", you have the option of following the terms and
|
|
||||||
conditions either of that version or of any later version published by
|
|
||||||
the Free Software Foundation. If the Library does not specify a
|
|
||||||
license version number, you may choose any version ever published by
|
|
||||||
the Free Software Foundation.
|
|
||||||
|
|
||||||
14. If you wish to incorporate parts of the Library into other free
|
|
||||||
programs whose distribution conditions are incompatible with these,
|
|
||||||
write to the author to ask for permission. For software which is
|
|
||||||
copyrighted by the Free Software Foundation, write to the Free
|
|
||||||
Software Foundation; we sometimes make exceptions for this. Our
|
|
||||||
decision will be guided by the two goals of preserving the free status
|
|
||||||
of all derivatives of our free software and of promoting the sharing
|
|
||||||
and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
|
||||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
|
||||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
|
||||||
OTHER PARTIES PROVIDE THE LIBRARY "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
|
|
||||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
|
||||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
|
||||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
|
||||||
AND/OR REDISTRIBUTE THE LIBRARY 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
|
|
||||||
LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
|
||||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
|
||||||
DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Libraries
|
|
||||||
|
|
||||||
If you develop a new library, and you want it to be of the greatest
|
|
||||||
possible use to the public, we recommend making it free software that
|
|
||||||
everyone can redistribute and change. You can do so by permitting
|
|
||||||
redistribution under these terms (or, alternatively, under the terms of the
|
|
||||||
ordinary General Public License).
|
|
||||||
|
|
||||||
To apply these terms, attach the following notices to the library. It is
|
|
||||||
safest to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least the
|
|
||||||
"copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the library's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library 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
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
|
||||||
USA
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
|
||||||
library `Frob' (a library for tweaking knobs) written by James Random
|
|
||||||
Hacker.
|
|
||||||
|
|
||||||
<signature of Ty Coon>, 1 April 1990
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
That's all there is to it!
|
|
||||||
44
README.md
44
README.md
|
|
@ -2,28 +2,30 @@
|
||||||
|
|
||||||
This is a Velocity plugin that makes it possible to host a modern Forge server behind a Velocity proxy!
|
This is a Velocity plugin that makes it possible to host a modern Forge server behind a Velocity proxy!
|
||||||
|
|
||||||
Unlike other solutions, this plugin does not require any special modifications to the backend server nor the client. (The player doesn't need to do anything)
|
Unlike other solutions, this plugin does not require any special modifications to the client. (The player doesn't need to do anything)
|
||||||
## Only for 1.13-1.20.1
|
|
||||||
Velocity has now added built-in support for newer mc versions and therefore don't need Ambassador. You might still need PCF mod on the server-side, for more info please visit: https://github.com/adde0109/Proxy-Compatible-Forge
|
|
||||||
## How to get started:
|
|
||||||
1. Download and install this plugin to your proxy.
|
|
||||||
2. After starting the server, configure the plugin it to your liking using the config file found in the folder "Ambassador".
|
|
||||||
3. If you want to use player-information forwarding you can use any of these mods on the 1.13+ forge server:
|
|
||||||
- https://github.com/adde0109/Proxy-Compatible-Forge (Modern forwarding) (Magma 1.18.2 and higher includes this)
|
|
||||||
|
|
||||||
- https://github.com/caunt/BungeeForge (Legacy forwarding)
|
This plugin is right now in its alpha stage and should not be used in production, use it at your own risk, you have been warned.
|
||||||
|
## How to get started:
|
||||||
|
### On the Velocity proxy side:
|
||||||
|
1. Download and install this plugin to your proxy.
|
||||||
|
2. Start the proxy, then close it.
|
||||||
|
3. Go to "plugins/ambassador" and open "ambassador.toml"
|
||||||
|
4. In the "Forge Server" field, put the name of the Forge server. The name must be the same as that you put in Velocity's "velocity.toml" config.
|
||||||
|
5. Now you are done on the proxy side!
|
||||||
|
|
||||||
|
### On the Forge server side:
|
||||||
|
1. Download and install "Ambassador-Forge" as a mod to your Forge server. (Found at https://github.com/adde0109/Ambassador-Forge)
|
||||||
|
2. Start the server.
|
||||||
|
3. If you wish to use modern forwarding, close the server and open "ambassador-common.toml" in the config folder and put your forwarding secret in the "forwardingSecret" field.
|
||||||
|
4. In "server.properties" make sure online-mode is set to false.
|
||||||
|
5. You are now ready to start the server and connect to it with Velocity!
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Server switching without any client side mod when the servers are similar. (Mods must match)
|
* Server Switching.
|
||||||
* ServerRedirect support for server switching.
|
* Force Forge clients to connect directly to Forge server if mods don't allow vanilla.
|
||||||
* Server switching using Client Reset Packet Mod for instant server switching:
|
* Connect to diffrent Forge servers depending on client version.
|
||||||
|
|
||||||
1.16.5: https://github.com/Just-Chaldea/Forge-Client-Reset-Packet
|
|
||||||
|
|
||||||
1.18.2+: https://www.curseforge.com/minecraft/mc-mods/forge-client-reset-packet-forward
|
## Planned features
|
||||||
|
* Ping Forwarding.
|
||||||
## Stuck on "Negotiating":
|
* Server switching using kick to reset the client.
|
||||||
This is an issue with Client Reset Packet Mod being partly incompatible with certain mods on the client. Please remove incompatible mods on the client if you have this issue. (Yes, this also includes client-side only mods.)
|
* Smart handshaking-data caching system to reduce load/network-traffic.
|
||||||
|
|
||||||
## Discord
|
|
||||||
https://discord.gg/Vusz9pBNyJ
|
|
||||||
|
|
|
||||||
1
Velocity
1
Velocity
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit c3583e182ca6585e40d1eef0da8c18547c0b1bc1
|
|
||||||
|
|
@ -5,7 +5,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "org.adde0109"
|
group = "org.adde0109"
|
||||||
version = "1.5.3-beta"
|
version = "0.5.1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
@ -16,24 +16,28 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("com.velocitypowered:velocity-api")
|
compileOnly("com.velocitypowered:velocity-api:3.2.0-SNAPSHOT")
|
||||||
compileOnly("com.velocitypowered:velocity-proxy")
|
|
||||||
annotationProcessor("com.velocitypowered:velocity-api")
|
|
||||||
compileOnly("com.electronwill.night-config:toml:3.6.6")
|
compileOnly("com.electronwill.night-config:toml:3.6.6")
|
||||||
implementation("org.bstats:bstats-velocity:3.0.1")
|
implementation("org.bstats:bstats-velocity:3.0.1")
|
||||||
compileOnly("io.netty:netty-buffer:4.1.90.Final")
|
compileOnly("org.apache.commons:commons-collections4:4.4")
|
||||||
compileOnly("io.netty:netty-transport:4.1.90.Final")
|
annotationProcessor("com.velocitypowered:velocity-api:3.2.0-SNAPSHOT")
|
||||||
compileOnly("io.netty:netty-codec:4.1.90.Final")
|
compileOnly("org.jetbrains:annotations:24.0.1")
|
||||||
compileOnly("io.netty:netty-handler:4.1.90.Final")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
jar {
|
|
||||||
dependsOn(shadowJar);
|
|
||||||
enabled = false;
|
|
||||||
}
|
|
||||||
shadowJar {
|
shadowJar {
|
||||||
relocate("org.bstats", "org.adde0109.ambassador")
|
relocate("org.bstats", "org.adde0109.ambassador")
|
||||||
archiveBaseName.set("Ambassador-Velocity")
|
archiveBaseName.set("Ambassador-Velocity")
|
||||||
|
archiveClassifier.set("")
|
||||||
|
}
|
||||||
|
jar.get().isEnabled = false
|
||||||
|
build {
|
||||||
|
dependsOn(shadowJar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(17))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
||||||
7
gradlew
vendored
7
gradlew
vendored
|
|
@ -85,6 +85,9 @@ done
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
|
@ -194,10 +197,6 @@ if "$cygwin" || "$msys" ; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command;
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1 @@
|
||||||
rootProject.name = "Ambassador"
|
rootProject.name = "Ambassador"
|
||||||
includeBuild("Velocity") {
|
|
||||||
dependencySubstitution {
|
|
||||||
substitute(module("com.velocitypowered:velocity-api")).using(project(":velocity-api"))
|
|
||||||
substitute(module("com.velocitypowered:velocity-proxy")).using(project(":velocity-proxy"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +1,36 @@
|
||||||
package org.adde0109.ambassador;
|
package org.adde0109.ambassador;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.velocitypowered.api.event.PostOrder;
|
import com.velocitypowered.api.event.Continuation;
|
||||||
import com.velocitypowered.api.event.Subscribe;
|
import com.velocitypowered.api.event.Subscribe;
|
||||||
|
import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
|
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
|
||||||
import com.velocitypowered.api.plugin.Plugin;
|
import com.velocitypowered.api.plugin.Plugin;
|
||||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||||
import com.velocitypowered.api.proxy.ProxyServer;
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
|
import org.adde0109.ambassador.forge.ForgeConnection;
|
||||||
import java.lang.reflect.Field;
|
import org.adde0109.ambassador.forge.ForgeHandshakeHandler;
|
||||||
import java.lang.reflect.Method;
|
import org.adde0109.ambassador.forge.ForgeHandshakeUtils;
|
||||||
import java.nio.file.Files;
|
import org.adde0109.ambassador.forge.ForgeServerSwitchHandler;
|
||||||
import java.util.HashMap;
|
import org.bstats.charts.SingleLineChart;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentPropertyRegistry;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentPropertySerializer;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.channel.Channel;
|
|
||||||
import io.netty.channel.ChannelInitializer;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
||||||
import org.adde0109.ambassador.velocity.VelocityBackendChannelInitializer;
|
|
||||||
import org.adde0109.ambassador.velocity.VelocityServerChannelInitializer;
|
|
||||||
import org.adde0109.ambassador.velocity.VelocityEventHandler;
|
|
||||||
import org.adde0109.ambassador.velocity.protocol.EnumArgumentProperty;
|
|
||||||
import org.adde0109.ambassador.velocity.protocol.EnumArgumentPropertySerializer;
|
|
||||||
import org.adde0109.ambassador.velocity.protocol.ModIdArgumentProperty;
|
|
||||||
import org.bstats.velocity.Metrics;
|
import org.bstats.velocity.Metrics;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19;
|
@Plugin(id = "ambassador", name = "Ambassador", version = "0.5.1", authors = {"adde0109"})
|
||||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
|
|
||||||
|
|
||||||
@Plugin(id = "ambassador", name = "Ambassador", version = "1.5.3-beta", authors = {"adde0109"})
|
|
||||||
public class Ambassador {
|
public class Ambassador {
|
||||||
|
|
||||||
//Don't forget to update checkCompatibleVersion() when changing this value
|
|
||||||
private static final String minVelocityVersion = "velocity-3.3.0-SNAPSHOT-330";
|
|
||||||
|
|
||||||
public ProxyServer server;
|
public ProxyServer server;
|
||||||
public final Logger logger;
|
public final Logger logger;
|
||||||
|
public AmbassadorConfig config;
|
||||||
private final Metrics.Factory metricsFactory;
|
private final Metrics.Factory metricsFactory;
|
||||||
private final Path dataDirectory;
|
private final Path dataDirectory;
|
||||||
|
|
||||||
public AmbassadorConfig config;
|
public ForgeHandshakeHandler forgeHandshakeHandler;
|
||||||
|
public ForgeServerSwitchHandler forgeServerSwitchHandler;
|
||||||
|
|
||||||
private static final MapWithExpiration<String, RegisteredServer> TEMPORARY_FORCED = new MapWithExpiration<>();
|
|
||||||
|
|
||||||
private static Ambassador instance;
|
|
||||||
public static Ambassador getInstance() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -73,124 +39,54 @@ public class Ambassador {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.dataDirectory = dataDirectory;
|
this.dataDirectory = dataDirectory;
|
||||||
this.metricsFactory = metricsFactory;
|
this.metricsFactory = metricsFactory;
|
||||||
Ambassador.instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean checkCompatibleVersion() {
|
|
||||||
//Update this when changing minVelocityVersion
|
|
||||||
try {
|
|
||||||
Class.forName("com.velocitypowered.proxy.protocol.packet.DisconnectPacket");
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe(order = PostOrder.LAST)
|
|
||||||
public void onProxyInitialization(ProxyInitializeEvent event) {
|
|
||||||
initMetrics();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!checkCompatibleVersion()) {
|
|
||||||
logger.error("Incompatible velocity version! Please use '" + minVelocityVersion + "' or higher.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.createDirectories(dataDirectory);
|
|
||||||
|
|
||||||
Path configPath = dataDirectory.resolve("Ambassador.toml");
|
|
||||||
config = AmbassadorConfig.read(configPath);
|
|
||||||
|
|
||||||
inject();
|
|
||||||
|
|
||||||
server.getEventManager().register(this, new VelocityEventHandler(this));
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("An error prevented Ambassador to load correctly: "+ e.toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onProxyReload(ProxyReloadEvent event) {
|
public void onProxyInitialization(ProxyInitializeEvent event) {
|
||||||
try {
|
initMetrics();
|
||||||
Path configPath = dataDirectory.resolve("Ambassador.toml");
|
|
||||||
final AmbassadorConfig newconfig = AmbassadorConfig.read(configPath);
|
|
||||||
|
|
||||||
config = newconfig;
|
config = AmbassadorConfig.readOrCreateConfig(dataDirectory,server,logger);
|
||||||
} catch (Exception e) {
|
if(config != null) {
|
||||||
logger.error(e.toString());
|
forgeHandshakeHandler = new ForgeHandshakeHandler(this);
|
||||||
logger.warn("Reload unsuccessful, old config will be used.");
|
forgeServerSwitchHandler = new ForgeServerSwitchHandler(this);
|
||||||
|
server.getEventManager().register(this, forgeHandshakeHandler);
|
||||||
|
server.getEventManager().register(this,forgeServerSwitchHandler);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.warn("Ambassador will be disabled because of errors");
|
||||||
|
}
|
||||||
|
|
||||||
|
ForgeHandshakeUtils.HandshakeReceiver.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onProxyReloadEvent(ProxyReloadEvent event) {
|
||||||
|
AmbassadorConfig c = AmbassadorConfig.readOrCreateConfig(dataDirectory,server,logger);
|
||||||
|
if (config != null) {
|
||||||
|
config = c;
|
||||||
|
logger.info("Successfully reloaded the config");
|
||||||
|
} else {
|
||||||
|
logger.warn("Using the old config");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inject() throws ReflectiveOperationException {
|
|
||||||
Field cmField = VelocityServer.class.getDeclaredField("cm");
|
|
||||||
cmField.setAccessible(true);
|
|
||||||
|
|
||||||
ConnectionManager connectionManager = (ConnectionManager) cmField.get(server);
|
@Subscribe
|
||||||
|
public void onPlayerChooseInitialServerEvent(PlayerChooseInitialServerEvent event, Continuation continuation) {
|
||||||
ChannelInitializer<Channel> original = connectionManager.getServerChannelInitializer().get();
|
//Only handle Forge connections
|
||||||
connectionManager.getServerChannelInitializer().set(new VelocityServerChannelInitializer(original, (VelocityServer) server));
|
if (forgeHandshakeHandler == null) return;
|
||||||
|
if((event.getInitialServer().isPresent()) && (forgeHandshakeHandler.getForgeConnection(event.getPlayer()).isPresent())) {
|
||||||
ChannelInitializer<Channel> originalBackend = connectionManager.getBackendChannelInitializer().get();
|
//Forge client
|
||||||
connectionManager.getBackendChannelInitializer().set(new VelocityBackendChannelInitializer(originalBackend, (VelocityServer) server));
|
ForgeConnection forgeConnection = forgeHandshakeHandler.getForgeConnection(event.getPlayer()).get();
|
||||||
|
if (forgeConnection.isForced()) {
|
||||||
Method argumentRegistry = ArgumentPropertyRegistry.class.getDeclaredMethod("register", ArgumentIdentifier.class, Class.class, ArgumentPropertySerializer.class);
|
event.setInitialServer(forgeConnection.getSyncedServer().get());
|
||||||
argumentRegistry.setAccessible(true);
|
}
|
||||||
argumentRegistry.invoke(null,ArgumentIdentifier.id("forge:enum", mapSet(MINECRAFT_1_19, 50)), EnumArgumentProperty.class, EnumArgumentPropertySerializer.ENUM);
|
forgeConnection.setForced(config.getForced(forgeConnection.getConnection().getProtocolVersion().getProtocol()));
|
||||||
argumentRegistry.invoke(null,ArgumentIdentifier.id("forge:modid", mapSet(MINECRAFT_1_19, 51)), ModIdArgumentProperty.class,
|
}
|
||||||
new ArgumentPropertySerializer<>() {
|
continuation.resume();
|
||||||
@Override
|
|
||||||
public ModIdArgumentProperty deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
|
||||||
return new ModIdArgumentProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void serialize(Object object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MapWithExpiration<String, RegisteredServer> getTemporaryForced() {
|
|
||||||
return TEMPORARY_FORCED;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reconnectSwitchPlayer(ConnectedPlayer player) {
|
|
||||||
TEMPORARY_FORCED.put(player.getUsername(), player.getConnectionInFlight().getServer(),
|
|
||||||
config.getServerSwitchCancellationTime(), TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
MiniMessage mm = MiniMessage.miniMessage();
|
|
||||||
Component parsed = mm.deserialize(config.getKickReconnectMessageString());
|
|
||||||
|
|
||||||
player.disconnect(parsed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initMetrics() {
|
private void initMetrics() {
|
||||||
Metrics metrics = metricsFactory.make(this, 15655);
|
metricsFactory.make(this, 15655).addCustomChart(new SingleLineChart("modern_forge_players", () -> (forgeHandshakeHandler != null) ? forgeHandshakeHandler.getAmountOfForgeConnections() : 0));
|
||||||
}
|
|
||||||
|
|
||||||
public static class MapWithExpiration<K, V> {
|
|
||||||
|
|
||||||
private final Map<K, ExpiringValue<V, Long>> expirationMap = new HashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
public V remove(K key) {
|
|
||||||
ExpiringValue<V, Long> expiringValue = expirationMap.remove(key);
|
|
||||||
if (expiringValue != null && expiringValue.value > System.currentTimeMillis()) {
|
|
||||||
return expiringValue.key;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void put(K key, V value, int expirationTime, TimeUnit unit) {
|
|
||||||
expirationMap.values().removeIf((v) -> v.value <= System.currentTimeMillis());
|
|
||||||
expirationMap.put(key, new ExpiringValue<>(value,System.currentTimeMillis() + unit.toMillis(expirationTime)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private record ExpiringValue<K, V>(K key, V value) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,164 @@
|
||||||
package org.adde0109.ambassador;
|
package org.adde0109.ambassador;
|
||||||
|
|
||||||
import com.electronwill.nightconfig.core.conversion.InvalidValueException;
|
import com.electronwill.nightconfig.core.CommentedConfig;
|
||||||
|
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||||
import com.google.gson.annotations.Expose;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class AmbassadorConfig {
|
public class AmbassadorConfig {
|
||||||
|
|
||||||
@Expose
|
private static final int CONFIG_VERSION = 1;
|
||||||
private int serverSwitchCancellationTime = 30;
|
private final ProxyServer server;
|
||||||
|
private final Logger logger;
|
||||||
|
private Differentiators differentiatorsSettings;
|
||||||
|
private ReSync reSyncSettings;
|
||||||
|
|
||||||
@Expose
|
|
||||||
private boolean silenceWarnings = false;
|
|
||||||
@Expose
|
|
||||||
private boolean bypassRegistryCheck = false;
|
|
||||||
@Expose
|
|
||||||
private boolean bypassModCheck = false;
|
|
||||||
|
|
||||||
@Expose
|
private AmbassadorConfig(ProxyServer server,Logger logger) {
|
||||||
private boolean debugMode = false;
|
this.server = server;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
@Expose
|
|
||||||
private boolean enableKickReset = false;
|
|
||||||
|
|
||||||
@Expose
|
|
||||||
private String kickReconnectMessageString = "<red>Please reconnect.</red>";
|
|
||||||
|
|
||||||
private AmbassadorConfig(boolean silenceWarnings, boolean bypassRegistryCheck, boolean bypassModCheck,
|
public RegisteredServer getServer(int protocolVersion) {
|
||||||
boolean debugMode, boolean enableKickReset, String kickReconnectMessageString) {
|
return differentiatorsSettings.differentiators.get(protocolVersion).handshakeServer;
|
||||||
this.silenceWarnings = silenceWarnings;
|
}
|
||||||
this.bypassRegistryCheck = bypassRegistryCheck;
|
|
||||||
this.bypassModCheck = bypassModCheck;
|
public boolean getForced (int protocolVersion) {
|
||||||
this.debugMode = debugMode;
|
return differentiatorsSettings.differentiators.get(protocolVersion).forced;
|
||||||
this.enableKickReset = enableKickReset;
|
}
|
||||||
this.kickReconnectMessageString = kickReconnectMessageString;
|
|
||||||
|
public boolean shouldHandle(int protocolVersion) {
|
||||||
|
return differentiatorsSettings.differentiators.containsKey(protocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReSyncTimeout() {
|
||||||
|
return reSyncSettings.reSyncTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reSyncOption reSyncOptionForge() {
|
||||||
|
return reSyncSettings.reSyncForgeForge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reSyncOption reSyncOptionVanilla() {
|
||||||
|
return reSyncSettings.reSyncForgeVanilla;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static AmbassadorConfig readOrCreateConfig(Path dataDirectory,ProxyServer server, Logger logger) {
|
||||||
|
AmbassadorConfig ambassadorConfig = new AmbassadorConfig(server,logger);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(dataDirectory);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (FileAlreadyExistsException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
logger.error("Config related error: " + e);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AmbassadorConfig read(Path path) throws IOException {
|
try {
|
||||||
URL defaultConfigLocation = AmbassadorConfig.class.getClassLoader()
|
CommentedFileConfig config = CommentedFileConfig.builder(dataDirectory.resolve("ambassador.toml"))
|
||||||
.getResource("default-ambassador.toml");
|
.defaultData(Ambassador.class.getClassLoader().getResource("default-ambassador.toml"))
|
||||||
if (defaultConfigLocation == null) {
|
.autosave()
|
||||||
throw new RuntimeException("Default configuration file does not exist.");
|
.preserveInsertionOrder()
|
||||||
|
.sync()
|
||||||
|
.build();
|
||||||
|
config.load();
|
||||||
|
|
||||||
|
if (config.getOrElse("config-version",0) != CONFIG_VERSION) {
|
||||||
|
throw new Exception("Incompatible config-version detected! Please delete 'ambassador.toml' and reload.");
|
||||||
|
}
|
||||||
|
CommentedConfig differentiatorsSettingsConfig = config.get("Differentiators");
|
||||||
|
CommentedConfig reSyncSettingsConfig = config.get("ReSync");
|
||||||
|
|
||||||
|
|
||||||
|
ambassadorConfig.differentiatorsSettings = ambassadorConfig.new Differentiators(differentiatorsSettingsConfig);
|
||||||
|
ambassadorConfig.reSyncSettings = ambassadorConfig.new ReSync(reSyncSettingsConfig);
|
||||||
|
|
||||||
|
|
||||||
|
config.save();
|
||||||
|
return ambassadorConfig;
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
logger.error("Config related error: " + e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReSync {
|
||||||
|
private int reSyncTimeout = 30;
|
||||||
|
private reSyncOption reSyncForgeForge = reSyncOption.ALWAYS;
|
||||||
|
private reSyncOption reSyncForgeVanilla = reSyncOption.NEVER;
|
||||||
|
|
||||||
|
private ReSync(CommentedConfig config) {
|
||||||
|
if (config != null) {
|
||||||
|
reSyncTimeout = config.getOrElse("resync-timeout",reSyncTimeout);
|
||||||
|
reSyncForgeForge = reSyncOption.valueOf(
|
||||||
|
config.getOrElse("resync-forge-to-forge",reSyncForgeForge.name()).toUpperCase());
|
||||||
|
}
|
||||||
|
reSyncForgeVanilla = reSyncOption.valueOf(
|
||||||
|
config.getOrElse("unsync-forge-to-vanilla",reSyncForgeVanilla.name()).toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum reSyncOption {
|
||||||
|
NEVER,
|
||||||
|
ASK,
|
||||||
|
ALWAYS
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Differentiators {
|
||||||
|
private Map<Integer,DifferentiatorSettings> differentiators = ImmutableMap.of(
|
||||||
|
758, new DifferentiatorSettings(),
|
||||||
|
754, new DifferentiatorSettings()
|
||||||
|
);
|
||||||
|
private Differentiators(){
|
||||||
|
}
|
||||||
|
|
||||||
|
private Differentiators(CommentedConfig config) throws Exception {
|
||||||
|
if (config != null) {
|
||||||
|
Map<Integer,DifferentiatorSettings> differentiators = new HashMap<>();
|
||||||
|
for (UnmodifiableConfig.Entry entry : config.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof CommentedConfig) {
|
||||||
|
differentiators.put(Integer.decode(entry.getKey()),new DifferentiatorSettings(entry.getValue()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this.differentiators = ImmutableMap.copyOf(differentiators);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CommentedFileConfig config = CommentedFileConfig.builder(path)
|
private class DifferentiatorSettings {
|
||||||
.defaultData(defaultConfigLocation)
|
private RegisteredServer handshakeServer = null;
|
||||||
.autosave()
|
private boolean forced = false;
|
||||||
.preserveInsertionOrder()
|
|
||||||
.sync()
|
|
||||||
.build();
|
|
||||||
config.load();
|
|
||||||
|
|
||||||
double configVersion;
|
private DifferentiatorSettings(){
|
||||||
try {
|
|
||||||
configVersion = Double.parseDouble(config.getOrElse("config-version", "1.0"));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
configVersion = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean silenceWarnings = config.getOrElse("silence-warnings", false);
|
|
||||||
|
|
||||||
int serverSwitchCancellationTime = config.getOrElse("serverRedirectTimeout", 30);
|
|
||||||
|
|
||||||
boolean bypassRegistryCheck = config.getOrElse("bypass-registry-checks", false);
|
|
||||||
|
|
||||||
boolean bypassModCheck = config.getOrElse("bypass-mod-checks", false);
|
|
||||||
|
|
||||||
boolean debugMode = config.getOrElse("debug-mode", false);
|
|
||||||
|
|
||||||
String kickReconnectMessageString = config.getOrElse("disconnect-reset-message",
|
|
||||||
config.getOrElse("reconnect-message", "<red>Please reconnect.</red>"));
|
|
||||||
|
|
||||||
//Upgrade config
|
|
||||||
if (configVersion <= 2.0) {
|
|
||||||
Files.delete(path);
|
|
||||||
config = CommentedFileConfig.builder(path)
|
|
||||||
.defaultData(defaultConfigLocation)
|
|
||||||
.autosave()
|
|
||||||
.preserveInsertionOrder()
|
|
||||||
.sync()
|
|
||||||
.build();
|
|
||||||
config.load();
|
|
||||||
config.set("silence-warnings", silenceWarnings);
|
|
||||||
config.set("serverRedirectTimeout", serverSwitchCancellationTime);
|
|
||||||
config.set("bypass-registry-checks", bypassRegistryCheck);
|
|
||||||
config.set("bypass-mod-checks", bypassModCheck);
|
|
||||||
config.set("debug-mode", debugMode);
|
|
||||||
config.set("reconnect-message", kickReconnectMessageString);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean enableKickReset = config.getOrElse("enable-kick-reset", false);
|
|
||||||
|
|
||||||
return new AmbassadorConfig(silenceWarnings, bypassRegistryCheck, bypassModCheck,
|
|
||||||
debugMode, enableKickReset, kickReconnectMessageString);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getServerSwitchCancellationTime() {
|
private DifferentiatorSettings(CommentedConfig config) throws Exception {
|
||||||
return serverSwitchCancellationTime;
|
if (config != null) {
|
||||||
|
String serverName = config.getOrElse("default-forge-server", "");
|
||||||
|
if (!Objects.equals(serverName, ""))
|
||||||
|
handshakeServer = server.getServer(serverName)
|
||||||
|
.orElseThrow(() -> new Exception(serverName + "is not a registered server!"));
|
||||||
|
this.forced = config.getOrElse("forced",forced);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSilenceWarnings() {
|
}
|
||||||
return silenceWarnings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isBypassRegistryCheck() {
|
|
||||||
return bypassRegistryCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isBypassModCheck() {
|
|
||||||
return bypassModCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDebugMode() {
|
|
||||||
return debugMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnableKickReset() {
|
|
||||||
return enableKickReset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKickReconnectMessageString() {
|
|
||||||
return kickReconnectMessageString;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
149
src/main/java/org/adde0109/ambassador/forge/ForgeConnection.java
Normal file
149
src/main/java/org/adde0109/ambassador/forge/ForgeConnection.java
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
package org.adde0109.ambassador.forge;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteArrayDataInput;
|
||||||
|
import com.velocitypowered.api.event.Continuation;
|
||||||
|
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
|
||||||
|
import com.velocitypowered.api.proxy.LoginPhaseConnection;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class ForgeConnection {
|
||||||
|
|
||||||
|
private final Logger logger;
|
||||||
|
private final LoginPhaseConnection connection;
|
||||||
|
|
||||||
|
private Optional<byte[]> recivedClientModlist = Optional.empty();
|
||||||
|
private static byte[] recivedClientACK;
|
||||||
|
private boolean ignoreSyncExepction = false;
|
||||||
|
|
||||||
|
private Optional<ForgeHandshakeUtils.CachedServerHandshake> transmittedHandshake = Optional.empty();
|
||||||
|
private boolean forced = false;
|
||||||
|
private Optional<RegisteredServer> syncedTo = Optional.empty();
|
||||||
|
|
||||||
|
|
||||||
|
public ForgeConnection(LoginPhaseConnection connection, Logger logger) {
|
||||||
|
this.connection = connection;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> testIfForge(LoginPhaseConnection connection) {
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
|
||||||
|
byte[] testPacket = ForgeHandshakeUtils.generateTestPacket();
|
||||||
|
//This gets also sent to vanilla
|
||||||
|
connection.sendLoginPluginMessage(MinecraftChannelIdentifier.create("fml", "loginwrapper"), testPacket,
|
||||||
|
responseBody -> {
|
||||||
|
future.complete(responseBody != null);
|
||||||
|
ignoreSyncExepction = responseBody == null;
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> sync(ForgeServerConnection forgeServerConnection) {
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
forgeServerConnection.getHandshake().whenComplete((msg,ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
future.complete(false);
|
||||||
|
logger.warn("Sync Exception: " + ex);
|
||||||
|
} else {
|
||||||
|
//This gets also sent to vanilla
|
||||||
|
sendModlist(msg.modListPacket).thenAccept((response) -> {
|
||||||
|
if (!ignoreSyncExepction && response == null) {
|
||||||
|
logger.warn("Sync Exception: Client responded with an empty body.");
|
||||||
|
}
|
||||||
|
recivedClientModlist = Optional.ofNullable(response);
|
||||||
|
});
|
||||||
|
//This gets also sent to vanilla
|
||||||
|
sendOther(msg.otherPackets).thenAccept((response) -> {
|
||||||
|
if (!ignoreSyncExepction && response == null) {
|
||||||
|
logger.warn("Sync Exception: Client responded with an empty body.");
|
||||||
|
}
|
||||||
|
//TODO: Generate the ACK packet ourself.
|
||||||
|
ForgeConnection.recivedClientACK = (response == null) ? ForgeConnection.recivedClientACK : response;
|
||||||
|
syncedTo = Optional.of(forgeServerConnection.getServer());
|
||||||
|
});
|
||||||
|
transmittedHandshake = Optional.of(msg);
|
||||||
|
future.complete(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<byte[]> sendModlist(byte[] modListPacket) {
|
||||||
|
CompletableFuture<byte[]> future = new CompletableFuture<>();
|
||||||
|
connection.sendLoginPluginMessage(MinecraftChannelIdentifier.create("fml", "loginwrapper"), modListPacket,
|
||||||
|
future::complete);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<byte[]> sendOther(List<byte[]> otherPackets) {
|
||||||
|
CompletableFuture<byte[]> future = new CompletableFuture<>();
|
||||||
|
for (int i = 0; i < otherPackets.size(); i++) {
|
||||||
|
connection.sendLoginPluginMessage(MinecraftChannelIdentifier.create("fml", "loginwrapper"), otherPackets.get(i),
|
||||||
|
(i < (otherPackets.size() - 1)) ? responseBody -> {
|
||||||
|
} : future::complete);
|
||||||
|
}
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleServerHandshakePacket(ServerLoginPluginMessageEvent event, Continuation continuation) {
|
||||||
|
ByteArrayDataInput data = event.contentsAsDataStream();
|
||||||
|
if (data.skipBytes(14) != 14) { //Channel Identifier
|
||||||
|
continuation.resumeWithException(new EOFException());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ForgeHandshakeUtils.readVarInt(data); //Length
|
||||||
|
int packetID = ForgeHandshakeUtils.readVarInt(data);
|
||||||
|
|
||||||
|
if (packetID == 1) {
|
||||||
|
if (getRecivedClientModlist().isPresent()) {
|
||||||
|
event.setResult(ServerLoginPluginMessageEvent.ResponseResult.reply(getRecivedClientModlist().get()));
|
||||||
|
} else {
|
||||||
|
continuation.resumeWithException(new Exception("Client isn't synced. This should have been caught" +
|
||||||
|
" during serverPreConnect"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (getRecivedClientACK() != null) {
|
||||||
|
event.setResult(ServerLoginPluginMessageEvent.ResponseResult.reply(getRecivedClientACK()));
|
||||||
|
} else {
|
||||||
|
continuation.resumeWithException(new Exception("No response available."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoginPhaseConnection getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ForgeHandshakeUtils.CachedServerHandshake> getTransmittedHandshake() {
|
||||||
|
return transmittedHandshake;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getRecivedClientModlist() {
|
||||||
|
return recivedClientModlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getRecivedClientACK() {
|
||||||
|
return recivedClientACK;
|
||||||
|
}
|
||||||
|
public Optional<RegisteredServer> getSyncedServer() {
|
||||||
|
return syncedTo;
|
||||||
|
}
|
||||||
|
public void setForced(boolean forced) {
|
||||||
|
this.forced = forced;
|
||||||
|
}
|
||||||
|
public boolean isForced() {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.connection.ConnectionType;
|
|
||||||
|
|
||||||
public class ForgeConstants {
|
|
||||||
public static final String HANDLER = "Modern Forge handler";
|
|
||||||
public static final String MARKER_ADDER = "FML2/3 Marker Adder";
|
|
||||||
public static final String RESET_LISTENER = "ambassador-reset-listener";
|
|
||||||
public static final String SERVER_SUCCESS_LISTENER = "ambassador-server-success-listener";
|
|
||||||
public static final String PLUGIN_PACKET_QUEUE = "ambassador-plugin-generated-packet-queue";
|
|
||||||
public static final String LOGIN_PACKET_QUEUE = "ambassador-login-packet-queue";
|
|
||||||
public static final String FORGE_HANDSHAKE_DECODER = "ambassador-forge-decoder";
|
|
||||||
public static final String FORGE_HANDSHAKE_HANDLER = "ambassador-forge-handler";
|
|
||||||
public static final String COMMAND_ERROR_CATCHER = "ambassador-command-catcher";
|
|
||||||
|
|
||||||
public static final String FML2Marker = "\0FML2\0";
|
|
||||||
public static final String FML3Marker = "\0FML3\0";
|
|
||||||
public static final ConnectionType ForgeFML2 = new ForgeFMLConnectionType(2);
|
|
||||||
public static final ConnectionType ForgeFML3 = new ForgeFMLConnectionType(3);
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.util.GameProfile;
|
|
||||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
|
||||||
import com.velocitypowered.proxy.connection.ConnectionType;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ClientConnectionPhase;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public class ForgeFMLConnectionType implements ConnectionType {
|
|
||||||
|
|
||||||
final int netVersion;
|
|
||||||
|
|
||||||
public ForgeFMLConnectionType(int netVersion) {
|
|
||||||
this.netVersion = netVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClientConnectionPhase getInitialClientPhase() {
|
|
||||||
return VelocityForgeClientConnectionPhase.NOT_STARTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BackendConnectionPhase getInitialBackendPhase() {
|
|
||||||
return VelocityForgeBackendConnectionPhase.NOT_STARTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GameProfile addGameProfileTokensIfRequired(GameProfile original, PlayerInfoForwarding forwardingType) {
|
|
||||||
//This is meant for Arclight to parse
|
|
||||||
if (forwardingType == PlayerInfoForwarding.LEGACY || forwardingType == PlayerInfoForwarding.BUNGEEGUARD) {
|
|
||||||
return original.addProperties(Collections.singleton(new GameProfile.Property("extraData", "\1FML" + netVersion + "\1", "")));
|
|
||||||
} else {
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge;
|
|
||||||
|
|
||||||
import org.adde0109.ambassador.forge.packet.Context;
|
|
||||||
import org.adde0109.ambassador.forge.packet.GenericForgeLoginWrapperPacket;
|
|
||||||
import org.adde0109.ambassador.forge.packet.ModListReplyPacket;
|
|
||||||
import org.adde0109.ambassador.forge.packet.RegistryPacket;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.zip.Adler32;
|
|
||||||
import java.util.zip.Checksum;
|
|
||||||
|
|
||||||
public class ForgeHandshake {
|
|
||||||
private ModListReplyPacket modListReplyPacket;
|
|
||||||
private final Map<String, Long> registries = new HashMap<>();
|
|
||||||
public GenericForgeLoginWrapperPacket<Context.ClientContext> zetaFlagsPacket;
|
|
||||||
|
|
||||||
public ForgeHandshake() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public ModListReplyPacket getModListReplyPacket() {
|
|
||||||
return modListReplyPacket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setModListReplyPacket(ModListReplyPacket modListReplyPacket) {
|
|
||||||
this.modListReplyPacket = modListReplyPacket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addRegistry(RegistryPacket packet) {
|
|
||||||
Checksum registryChecksum = new Adler32();
|
|
||||||
registryChecksum.update(packet.getSnapshot());
|
|
||||||
registries.put(packet.getRegistryName(), registryChecksum.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Long> getRegistries() {
|
|
||||||
return registries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCompatible(ForgeHandshake handshake) {
|
|
||||||
return this.registries.equals(handshake.registries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
package org.adde0109.ambassador.forge;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.event.Continuation;
|
||||||
|
import com.velocitypowered.api.event.PostOrder;
|
||||||
|
import com.velocitypowered.api.event.Subscribe;
|
||||||
|
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||||
|
import com.velocitypowered.api.event.player.KickedFromServerEvent;
|
||||||
|
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
|
||||||
|
import com.velocitypowered.api.proxy.LoginPhaseConnection;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.TranslatableComponent;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.adde0109.ambassador.Ambassador;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class ForgeHandshakeHandler {
|
||||||
|
|
||||||
|
private final Ambassador ambassador;
|
||||||
|
|
||||||
|
private final Map<RegisteredServer, ForgeServerConnection>
|
||||||
|
forgeServerConnectionMap = new HashMap<>();
|
||||||
|
private final Map<InetSocketAddress,ForgeConnection> incomingForgeConnections = new HashMap<>();
|
||||||
|
|
||||||
|
private static final ChannelIdentifier LOGIN_WRAPPER_ID = MinecraftChannelIdentifier.create("fml","loginwrapper");
|
||||||
|
|
||||||
|
|
||||||
|
public ForgeHandshakeHandler(Ambassador ambassador) {
|
||||||
|
this.ambassador = ambassador;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(order = PostOrder.LAST)
|
||||||
|
public void onPreLoginEvent(PreLoginEvent event, Continuation continuation) {
|
||||||
|
if (!ambassador.config.shouldHandle(event.getConnection().getProtocolVersion().getProtocol()) || !event.getResult().isAllowed()) {
|
||||||
|
continuation.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RegisteredServer defaultServer = ambassador.config.getServer(event.getConnection().getProtocolVersion().getProtocol());
|
||||||
|
|
||||||
|
ForgeConnection forgeConnection = new ForgeConnection((LoginPhaseConnection) event.getConnection(), ambassador.logger);
|
||||||
|
forgeConnection.testIfForge((LoginPhaseConnection) event.getConnection())
|
||||||
|
.thenAccept((isForge) -> {
|
||||||
|
if (isForge)
|
||||||
|
registerForgeConnection(forgeConnection);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ambassador.forgeServerSwitchHandler.reSyncMap.containsKey(event.getUsername())) {
|
||||||
|
forgeConnection.sync(ambassador.forgeServerSwitchHandler.reSyncMap.remove(event.getUsername())).thenAccept((done) -> continuation.resume());
|
||||||
|
forgeConnection.setForced(true);
|
||||||
|
} else if (defaultServer != null) {
|
||||||
|
//If a connection does not already exist, create one.
|
||||||
|
if (!forgeServerConnectionMap.containsKey(defaultServer)) {
|
||||||
|
forgeServerConnectionMap.put(defaultServer, new ForgeServerConnection(defaultServer));
|
||||||
|
}
|
||||||
|
//Forge Handshake
|
||||||
|
forgeConnection.sync(forgeServerConnectionMap.get(defaultServer)).thenAccept((done) -> continuation.resume());
|
||||||
|
forgeConnection.setForced(ambassador.config.getForced(forgeConnection.getConnection().getProtocolVersion().getProtocol()));
|
||||||
|
} else {
|
||||||
|
continuation.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerForgeConnection(ForgeConnection forgeConnection) {
|
||||||
|
if (forgeConnection != null) {
|
||||||
|
incomingForgeConnections.values().removeIf((c) -> !c.getConnection().isActive());
|
||||||
|
incomingForgeConnections.put(forgeConnection.getConnection().getRemoteAddress(), forgeConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ForgeConnection> getForgeConnection(Player player) {
|
||||||
|
return getForgeConnection(player.getRemoteAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ForgeConnection> getForgeConnection(InetSocketAddress socketAddress) {
|
||||||
|
incomingForgeConnections.values().removeIf((c) -> !c.getConnection().isActive());
|
||||||
|
return Optional.ofNullable(incomingForgeConnections.get(socketAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAmountOfForgeConnections() {
|
||||||
|
return incomingForgeConnections.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ForgeServerConnection> getForgeServerConnection(RegisteredServer registeredServer) {
|
||||||
|
return Optional.ofNullable(forgeServerConnectionMap.get(registeredServer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerForgeServer(RegisteredServer server, ForgeServerConnection forgeServerConnection) {
|
||||||
|
forgeServerConnectionMap.put(server,forgeServerConnection);
|
||||||
|
}
|
||||||
|
public void unRegisterForgeServer(RegisteredServer server) {
|
||||||
|
forgeServerConnectionMap.remove(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onServerLoginPluginMessageEvent(ServerLoginPluginMessageEvent event, Continuation continuation) {
|
||||||
|
if (!event.getIdentifier().equals(LOGIN_WRAPPER_ID)) {
|
||||||
|
continuation.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Check 2
|
||||||
|
if (getForgeServerConnection(event.getConnection().getServer()).isEmpty()) {
|
||||||
|
registerForgeServer(event.getConnection().getServer(),
|
||||||
|
new ForgeServerConnection(event.getConnection().getServer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingForgeConnections.containsKey(event.getConnection().getPlayer().getRemoteAddress())) {
|
||||||
|
incomingForgeConnections.get(event.getConnection().getPlayer().getRemoteAddress())
|
||||||
|
.handleServerHandshakePacket(event,continuation);
|
||||||
|
} else {
|
||||||
|
//This will lead to "multiplayer.disconnect.unexpected_query_response"
|
||||||
|
//and will be handled during KickFromServerEvent.
|
||||||
|
continuation.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onKickedFromServerEvent(KickedFromServerEvent event, Continuation continuation) {
|
||||||
|
Optional<ForgeConnection> forgeConnectionOptional = getForgeConnection(event.getPlayer());
|
||||||
|
if (forgeConnectionOptional.isPresent()) {
|
||||||
|
if (forgeConnectionOptional.get().isForced() && event.getResult() instanceof KickedFromServerEvent.RedirectPlayer) {
|
||||||
|
event.setResult(KickedFromServerEvent.DisconnectPlayer.create(event.getServerKickReason().get()));
|
||||||
|
}
|
||||||
|
} else if (event.getServerKickReason().isPresent()) {
|
||||||
|
Component reason = event.getServerKickReason().get();
|
||||||
|
if (reason instanceof TranslatableComponent)
|
||||||
|
if (((TranslatableComponent) reason).key().equals("multiplayer.disconnect.unexpected_query_response")) {
|
||||||
|
if (getForgeServerConnection(event.getServer()).isPresent()) {
|
||||||
|
//Turns out the server the vanilla client is connecting to is forge. Let's handle the connection error.
|
||||||
|
ambassador.logger.info("Vanilla player {} tried to connect to forge server {}. The connection error can be ignored.",
|
||||||
|
event.getPlayer(),event.getServer().getServerInfo().getName());
|
||||||
|
KickedFromServerEvent.ServerKickResult result = event.getResult();
|
||||||
|
Component component = Component.text("The server you were trying to connect to requires Forge to be installed.", NamedTextColor.RED);
|
||||||
|
if (result instanceof KickedFromServerEvent.DisconnectPlayer) {
|
||||||
|
event.setResult(KickedFromServerEvent.DisconnectPlayer.create(component));
|
||||||
|
} else if (result instanceof KickedFromServerEvent.RedirectPlayer) {
|
||||||
|
RegisteredServer redirectServer = ((KickedFromServerEvent.RedirectPlayer)event.getResult()).getServer();
|
||||||
|
event.setResult(KickedFromServerEvent.RedirectPlayer.create(redirectServer,component));
|
||||||
|
} else if (result instanceof KickedFromServerEvent.Notify) {
|
||||||
|
event.setResult(KickedFromServerEvent.Notify.create(component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,14 @@ package org.adde0109.ambassador.forge;
|
||||||
import com.google.common.io.ByteArrayDataInput;
|
import com.google.common.io.ByteArrayDataInput;
|
||||||
import com.google.common.io.ByteArrayDataOutput;
|
import com.google.common.io.ByteArrayDataOutput;
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import io.netty.buffer.ByteBuf;
|
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||||
import io.netty.buffer.Unpooled;
|
import com.velocitypowered.api.util.ModInfo;
|
||||||
import org.adde0109.ambassador.forge.packet.Context;
|
import java.util.*;
|
||||||
import org.adde0109.ambassador.forge.packet.GenericForgeLoginWrapperPacket;
|
import org.slf4j.Logger;
|
||||||
import org.adde0109.ambassador.forge.packet.IForgeLoginWrapperPacket;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public class ForgeHandshakeUtils {
|
public class ForgeHandshakeUtils {
|
||||||
|
|
||||||
|
|
@ -60,117 +60,158 @@ public class ForgeHandshakeUtils {
|
||||||
return stream.toByteArray();
|
return stream.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] generateResetPacket() {
|
public static class HandshakeReceiver {
|
||||||
ByteArrayDataOutput dataAndPacketIdStream = ByteStreams.newDataOutput();
|
|
||||||
writeVarInt(dataAndPacketIdStream,98);
|
|
||||||
|
|
||||||
ByteArrayDataOutput stream = ByteStreams.newDataOutput();
|
private int partLength;
|
||||||
byte[] dataAndPacketId = dataAndPacketIdStream.toByteArray();
|
|
||||||
writeUtf(stream,"fml:handshake");
|
|
||||||
writeVarInt(stream,dataAndPacketId.length);
|
|
||||||
stream.write(dataAndPacketId);
|
|
||||||
return stream.toByteArray();
|
|
||||||
}
|
|
||||||
public static byte[] generatePluginResetPacket() {
|
|
||||||
ByteArrayDataOutput dataAndPacketIdStream = ByteStreams.newDataOutput();
|
|
||||||
writeVarInt(dataAndPacketIdStream,98);
|
|
||||||
return dataAndPacketIdStream.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final byte[] emptyModlistFML2 = generateEmptyModlist(2);
|
public static Logger logger;
|
||||||
public static final byte[] emptyModlistFML3 = generateEmptyModlist(3);
|
|
||||||
private static byte[] generateEmptyModlist(int fmlVersion) {
|
private int numberOfRecivedParts;
|
||||||
ByteArrayDataOutput dataAndPacketIdStream = ByteStreams.newDataOutput();
|
private int recivedBytes;
|
||||||
writeVarInt(dataAndPacketIdStream,1);
|
|
||||||
writeVarInt(dataAndPacketIdStream,0); //Mods
|
private final int numberOfParts;
|
||||||
if (fmlVersion == 3) {
|
private final long checksum;
|
||||||
writeVarInt(dataAndPacketIdStream,1); //Channels
|
private final int[] separators;
|
||||||
writeUtf(dataAndPacketIdStream, "forge:tier_sorting");
|
private final byte[] recivedParts;
|
||||||
writeUtf(dataAndPacketIdStream,"1.0");
|
|
||||||
writeVarInt(dataAndPacketIdStream,0); //Registries
|
|
||||||
writeVarInt(dataAndPacketIdStream,0); //Data-packs
|
private HandshakeReceiver(ServerPing serverPing) throws Exception {
|
||||||
} else {
|
if ((serverPing.getModinfo().isEmpty()) || (!Objects.equals(serverPing.getModinfo().get().getType(), "ambassador"))) {
|
||||||
writeVarInt(dataAndPacketIdStream,0); //Channels
|
throw new HandshakeNotAvailableException("The specified Forge server is not running the Ambassador-Forge mod!");
|
||||||
writeVarInt(dataAndPacketIdStream,0); //Registries
|
}
|
||||||
|
|
||||||
|
ModInfo.Mod pair = serverPing.getModinfo().orElseThrow(IllegalAccessError::new).getMods().get(0);
|
||||||
|
|
||||||
|
this.separators = Arrays.stream(pair.getVersion().substring(pair.getVersion().indexOf(":") + 1).split(":")).map(Integer::parseInt).mapToInt(x -> x).toArray();
|
||||||
|
this.checksum = Long.parseUnsignedLong((pair.getVersion().split(":")[0].split("-"))[3],16);
|
||||||
|
this.numberOfParts = Integer.parseInt((pair.getVersion().split(":")[0].split("-"))[1]);
|
||||||
|
int totalLength = Integer.parseInt((pair.getVersion().split(":")[0].split("-"))[2]);
|
||||||
|
this.recivedParts = new byte[totalLength];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ByteArrayDataOutput stream = ByteStreams.newDataOutput();
|
|
||||||
byte[] dataAndPacketId = dataAndPacketIdStream.toByteArray();
|
|
||||||
writeUtf(stream,"fml:handshake");
|
|
||||||
writeVarInt(stream,dataAndPacketId.length);
|
|
||||||
stream.write(dataAndPacketId);
|
|
||||||
return stream.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public static CompletableFuture<CachedServerHandshake> downloadHandshake(RegisteredServer forgeServer) {
|
||||||
public static final byte[] ACKPacket = generateACKPacket();
|
CompletableFuture<CachedServerHandshake> future = new CompletableFuture<>();
|
||||||
private static byte[] generateACKPacket() {
|
forgeServer.ping().whenComplete((msg,ex) -> {
|
||||||
ByteArrayDataOutput dataAndPacketIdStream = ByteStreams.newDataOutput();
|
if (ex != null) {
|
||||||
writeVarInt(dataAndPacketIdStream,99);
|
future.completeExceptionally(ex);
|
||||||
|
} else {
|
||||||
ByteArrayDataOutput stream = ByteStreams.newDataOutput();
|
try {
|
||||||
byte[] dataAndPacketId = dataAndPacketIdStream.toByteArray();
|
HandshakeReceiver handshakeReceiver = new HandshakeReceiver(msg);
|
||||||
writeUtf(stream,"fml:handshake");
|
handshakeReceiver.handle(msg);
|
||||||
writeVarInt(stream,dataAndPacketId.length);
|
handshakeReceiver.downloadLoop(forgeServer,future);
|
||||||
stream.write(dataAndPacketId);
|
} catch (Exception e) {
|
||||||
return stream.toByteArray();
|
future.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ThirdPartyRegistryUtils {
|
|
||||||
|
|
||||||
static enum ThirdPartyChannel {
|
|
||||||
SILENTGEAR_NETWORK {
|
|
||||||
@Override
|
|
||||||
public IForgeLoginWrapperPacket<Context.ClientContext> generateResponsePacket(Context.ClientContext context, ForgeHandshake completed) {
|
|
||||||
return new ACKPacket(context, 3);
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
ZETA_MAIN {
|
return future;
|
||||||
@Override
|
}
|
||||||
public IForgeLoginWrapperPacket<Context.ClientContext> generateResponsePacket(Context.ClientContext context, ForgeHandshake completed) {
|
|
||||||
return new GenericForgeLoginWrapperPacket<Context.ClientContext>(completed.zetaFlagsPacket.getContent(), context);
|
public static CompletableFuture<CachedServerHandshake> downloadHandshake(RegisteredServer forgeServer, CachedServerHandshake oldHandshake) {
|
||||||
|
CompletableFuture<CachedServerHandshake> future = new CompletableFuture<>();
|
||||||
|
forgeServer.ping().whenComplete((msg,ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
future.completeExceptionally(ex);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
HandshakeReceiver handshakeReceiver = new HandshakeReceiver(msg);
|
||||||
|
if (handshakeReceiver.getChecksum() == oldHandshake.fingerprint) {
|
||||||
|
future.complete(oldHandshake);
|
||||||
|
} else {
|
||||||
|
handshakeReceiver.handle(msg);
|
||||||
|
handshakeReceiver.downloadLoop(forgeServer,future);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
abstract public IForgeLoginWrapperPacket<Context.ClientContext> generateResponsePacket(Context.ClientContext context, ForgeHandshake completed);
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isThirdPartyPacket(GenericForgeLoginWrapperPacket<Context> packet) {
|
private long getChecksum() {
|
||||||
try {
|
return checksum;
|
||||||
Enum.valueOf(ThirdPartyChannel.class,
|
}
|
||||||
packet.getContext().getChannelName().replace(':', '_').toUpperCase());
|
|
||||||
return true;
|
private void downloadLoop(RegisteredServer server, CompletableFuture<CachedServerHandshake> future) {
|
||||||
} catch (IllegalArgumentException e) {
|
if (numberOfRecivedParts < numberOfParts) {
|
||||||
return false;
|
server.ping().whenComplete((msg,ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
future.completeExceptionally(ex);
|
||||||
|
} else {
|
||||||
|
handle(msg);
|
||||||
|
downloadLoop(server, future);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
List<byte[]> packets = splitPackets(recivedParts,separators);
|
||||||
|
future.complete(new CachedServerHandshake(checksum,packets.get(0),packets.subList(1,packets.size()-1)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ThirdPartyChannel getThirdPartyChannel(GenericForgeLoginWrapperPacket<Context> packet) {
|
|
||||||
return Enum.valueOf(ThirdPartyChannel.class,
|
private void handle(ServerPing status) {
|
||||||
packet.getContext().getChannelName().replace(':', '_').toUpperCase());
|
numberOfRecivedParts++;
|
||||||
|
|
||||||
|
ModInfo.Mod pair = status.getModinfo().orElseThrow(IllegalAccessError::new).getMods().get(0);
|
||||||
|
int recivedPartNr = Integer.parseInt((pair.getVersion().split(":")[0].split("-"))[0]);
|
||||||
|
placePartInArray(pair.getId().getBytes(StandardCharsets.ISO_8859_1), recivedPartNr - 1);
|
||||||
|
|
||||||
|
logger.info("Downloaded part " + numberOfRecivedParts + " out of " + numberOfParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ACKPacket implements IForgeLoginWrapperPacket<Context.ClientContext> {
|
|
||||||
|
|
||||||
private final Context.ClientContext context;
|
|
||||||
private final int packetID;
|
private void placePartInArray(byte[] temp, int partNr) {
|
||||||
ACKPacket(Context.ClientContext context, int packetID) {
|
int head = (partNr == numberOfParts-1) ? recivedParts.length-temp.length : partNr*temp.length;
|
||||||
this.context = context;
|
for (byte b : temp) {
|
||||||
this.packetID = packetID;
|
recivedParts[head] = b;
|
||||||
|
head++;
|
||||||
|
recivedBytes++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ByteBuf encode() {
|
private byte[] getPacket(byte[] data, int startByteIndex, int lastByteIndex) {
|
||||||
ByteBuf buf = Unpooled.buffer();
|
byte[] temp = new byte[lastByteIndex - startByteIndex + 1];
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, packetID);
|
if (lastByteIndex + 1 - startByteIndex >= 0)
|
||||||
|
System.arraycopy(data, startByteIndex, temp, 0,
|
||||||
|
lastByteIndex + 1 - startByteIndex);
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
return buf;
|
|
||||||
|
private List<byte[]> splitPackets(byte[] data, int[] startPacketMarkers) {
|
||||||
|
List<byte[]> list = new ArrayList<>();
|
||||||
|
for (int i = 0; i < startPacketMarkers.length - 1; i++) {
|
||||||
|
list.add(getPacket(data, startPacketMarkers[i], startPacketMarkers[i + 1] - 1));
|
||||||
}
|
}
|
||||||
|
list.add(getPacket(data, startPacketMarkers[startPacketMarkers.length - 1], recivedBytes - 1));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
public static class HandshakeNotAvailableException extends Exception {
|
||||||
public Context.ClientContext getContext() {
|
HandshakeNotAvailableException(String errorMessage) {
|
||||||
return context;
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public static class CachedServerHandshake {
|
||||||
|
private final long fingerprint;
|
||||||
|
public byte[] modListPacket;
|
||||||
|
public List<byte[]> otherPackets;
|
||||||
|
|
||||||
|
private CachedServerHandshake(long fingerprint,byte[] modListPacket,List<byte[]> otherPackets) {
|
||||||
|
this.fingerprint = fingerprint;
|
||||||
|
this.modListPacket = modListPacket;
|
||||||
|
this.otherPackets = otherPackets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(CachedServerHandshake cachedServerHandshake) {
|
||||||
|
return this.fingerprint == cachedServerHandshake.fingerprint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.adde0109.ambassador.forge;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
|
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class ForgeServerConnection {
|
||||||
|
|
||||||
|
private static final int PACKET_LENGTH_INDEX = 14; //length of "fml:handshake"+1
|
||||||
|
private final RegisteredServer handshakeServer;
|
||||||
|
private ForgeHandshakeUtils.CachedServerHandshake handshake;
|
||||||
|
|
||||||
|
public RegisteredServer getServer() {
|
||||||
|
return handshakeServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ForgeServerConnection(RegisteredServer handshakeServer) {
|
||||||
|
this.handshakeServer = handshakeServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<ForgeHandshakeUtils.CachedServerHandshake> getHandshake() {
|
||||||
|
CompletableFuture<ForgeHandshakeUtils.CachedServerHandshake> future;
|
||||||
|
if (handshake == null) {
|
||||||
|
future = ForgeHandshakeUtils.HandshakeReceiver.downloadHandshake(handshakeServer);
|
||||||
|
} else {
|
||||||
|
future = ForgeHandshakeUtils.HandshakeReceiver.downloadHandshake(handshakeServer,handshake);
|
||||||
|
}
|
||||||
|
future.thenAccept(p -> handshake = p);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerInfo getServerInfo() {
|
||||||
|
return handshakeServer.getServerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package org.adde0109.ambassador.forge;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.event.Continuation;
|
||||||
|
import com.velocitypowered.api.event.PostOrder;
|
||||||
|
import com.velocitypowered.api.event.Subscribe;
|
||||||
|
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.util.GameProfile;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.adde0109.ambassador.Ambassador;
|
||||||
|
|
||||||
|
import org.adde0109.ambassador.AmbassadorConfig;
|
||||||
|
import org.apache.commons.collections4.map.PassiveExpiringMap;
|
||||||
|
|
||||||
|
public class ForgeServerSwitchHandler {
|
||||||
|
private final Ambassador ambassador;
|
||||||
|
public final PassiveExpiringMap<String,ForgeServerConnection> reSyncMap;
|
||||||
|
|
||||||
|
public ForgeServerSwitchHandler(Ambassador ambassador) {
|
||||||
|
this.ambassador = ambassador;
|
||||||
|
this.reSyncMap = new PassiveExpiringMap<>(ambassador.config.getReSyncTimeout(),TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Subscribe(order = PostOrder.LAST)
|
||||||
|
public void onServerPreConnectEvent(ServerPreConnectEvent event, Continuation continuation) {
|
||||||
|
if (!event.getResult().isAllowed()) {
|
||||||
|
continuation.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<ForgeServerConnection> forgeServerConnectionOptional = ambassador.forgeHandshakeHandler.getForgeServerConnection(event.getOriginalServer());
|
||||||
|
Optional<ForgeConnection> forgeConnection = ambassador.forgeHandshakeHandler.getForgeConnection(event.getPlayer());
|
||||||
|
if (forgeConnection.isPresent()) {
|
||||||
|
ForgeServerConnection forgeServerConnection = forgeServerConnectionOptional.orElseGet(() -> new ForgeServerConnection(event.getOriginalServer()));
|
||||||
|
forgeServerConnection.getHandshake().whenComplete((msg, ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
//The server was forge but aren't right now. Or it's just offline.
|
||||||
|
if (ex instanceof ForgeHandshakeUtils.HandshakeReceiver.HandshakeNotAvailableException) {
|
||||||
|
//It's not running ambassador, so it should be unregistered.
|
||||||
|
if (forgeServerConnectionOptional.isPresent())
|
||||||
|
ambassador.forgeHandshakeHandler.unRegisterForgeServer(forgeServerConnection.getServer());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//If the server just got discovered, register it.
|
||||||
|
if (forgeServerConnectionOptional.isEmpty())
|
||||||
|
ambassador.forgeHandshakeHandler.registerForgeServer(event.getOriginalServer(),forgeServerConnection);
|
||||||
|
|
||||||
|
//To make legacy forwarding work
|
||||||
|
List<GameProfile.Property> properties = new ArrayList<>(event.getPlayer().getGameProfileProperties());
|
||||||
|
properties.add(new GameProfile.Property("extraData", "\1FML2\1",""));
|
||||||
|
event.getPlayer().setGameProfileProperties(properties);
|
||||||
|
|
||||||
|
if (ambassador.config.reSyncOptionForge() != AmbassadorConfig.reSyncOption.NEVER) {
|
||||||
|
if (forgeConnection.get().getTransmittedHandshake().isEmpty() || !msg.equals(forgeConnection.get().getTransmittedHandshake().get())) {
|
||||||
|
event.setResult(ServerPreConnectEvent.ServerResult.denied());
|
||||||
|
reSync(event.getPlayer(),forgeServerConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.resume();
|
||||||
|
});
|
||||||
|
} else if (forgeServerConnectionOptional.isPresent()) {
|
||||||
|
//If vanilla tries to connect to a server we know is forge
|
||||||
|
event.setResult(ServerPreConnectEvent.ServerResult.denied());
|
||||||
|
event.getPlayer().sendMessage(Component.text("This server requires Forge!", NamedTextColor.RED));
|
||||||
|
continuation.resume();
|
||||||
|
} else {
|
||||||
|
//The server is not known to us.
|
||||||
|
continuation.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void reSync(Player player, ForgeServerConnection forgeServerConnection) {
|
||||||
|
ambassador.logger.info("Kicking {} because of re-sync needed", player);
|
||||||
|
player.disconnect(Component.text("Please reconnect"));
|
||||||
|
reSyncMap.put(player.getUsername(),forgeServerConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import org.adde0109.ambassador.forge.packet.*;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.zip.Adler32;
|
|
||||||
import java.util.zip.Checksum;
|
|
||||||
|
|
||||||
public class ShadowHandshakeReceiver {
|
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
|
||||||
private final ModListReplyPacket modListReplyPacket;
|
|
||||||
private final Map<String, Long> registries;
|
|
||||||
|
|
||||||
private ShadowHandshakeReceiver(ConnectedPlayer player, ModListReplyPacket modListReplyPacket,
|
|
||||||
Map<String, Long> registries) {
|
|
||||||
this.player = player;
|
|
||||||
this.modListReplyPacket = modListReplyPacket;
|
|
||||||
this.registries = registries;
|
|
||||||
}
|
|
||||||
|
|
||||||
void handle(IForgeLoginWrapperPacket packet) throws IncompatibleHandshake {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void handle(ModListPacket packet) throws IncompatibleHandshake {
|
|
||||||
|
|
||||||
//player.getConnection().write();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handle(RegistryPacket packet) throws IncompatibleHandshake {
|
|
||||||
|
|
||||||
player.getConnection().write(new ACKPacket(Context.fromContext(packet.getContext(), true)));
|
|
||||||
}
|
|
||||||
void handle(ConfigDataPacket packet) throws IncompatibleHandshake {
|
|
||||||
|
|
||||||
player.getConnection().write(new ACKPacket(Context.fromContext(packet.getContext(), true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Builder {
|
|
||||||
|
|
||||||
ConnectedPlayer player;
|
|
||||||
ModListReplyPacket modListReplyPacket;
|
|
||||||
Map<String, Long> registries = new HashMap<>();
|
|
||||||
|
|
||||||
void addModListPacket(ModListPacket packet) {
|
|
||||||
|
|
||||||
}
|
|
||||||
void setModListReplyPacket(ModListReplyPacket packet) {
|
|
||||||
this.modListReplyPacket = packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addRegistryPacket(RegistryPacket packet) {
|
|
||||||
Checksum registryChecksum = new Adler32();
|
|
||||||
registryChecksum.update(packet.getSnapshot());
|
|
||||||
registries.put(packet.getRegistryName(), registryChecksum.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
ShadowHandshakeReceiver build() {
|
|
||||||
return new ShadowHandshakeReceiver(player, modListReplyPacket, registries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class IncompatibleHandshake extends Throwable {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import org.adde0109.ambassador.Ambassador;
|
|
||||||
import org.adde0109.ambassador.forge.packet.*;
|
|
||||||
import org.adde0109.ambassador.forge.pipeline.CommandDecoderErrorCatcher;
|
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
|
|
||||||
public enum VelocityForgeBackendConnectionPhase implements BackendConnectionPhase {
|
|
||||||
NOT_STARTED {
|
|
||||||
@Override
|
|
||||||
VelocityForgeBackendConnectionPhase nextPhase() {
|
|
||||||
return IN_PROGRESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean consideredComplete() {
|
|
||||||
//Safe if the server hasn't initiated the handshake yet.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
IN_PROGRESS {
|
|
||||||
@Override
|
|
||||||
public void onLoginSuccess(VelocityServerConnection serverCon, ConnectedPlayer player) {
|
|
||||||
serverCon.setConnectionPhase(VelocityForgeBackendConnectionPhase.COMPLETE);
|
|
||||||
|
|
||||||
serverCon.getConnection().getChannel().pipeline().addBefore(Connections.MINECRAFT_DECODER,
|
|
||||||
ForgeConstants.COMMAND_ERROR_CATCHER,
|
|
||||||
new CommandDecoderErrorCatcher(serverCon.getConnection().getProtocolVersion(),player));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void onTransitionToNewPhase(VelocityServerConnection connection) {
|
|
||||||
MinecraftConnection mc = connection.getConnection();
|
|
||||||
if (mc != null) {
|
|
||||||
//This looks ugly. But unless the player didn't have a FML marker, we're fine.
|
|
||||||
mc.setType(connection.getPlayer().getConnection().getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
COMPLETE {
|
|
||||||
@Override
|
|
||||||
public boolean consideredComplete() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public ForgeHandshake handshake = new ForgeHandshake();
|
|
||||||
CountDownLatch remainingRegistries;
|
|
||||||
|
|
||||||
VelocityForgeBackendConnectionPhase() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handle(VelocityServerConnection server, ConnectedPlayer player, IForgeLoginWrapperPacket<Context> message) {
|
|
||||||
VelocityForgeBackendConnectionPhase newPhase = getNewPhase(server,message);
|
|
||||||
|
|
||||||
server.setConnectionPhase(newPhase);
|
|
||||||
|
|
||||||
//Forge -> Forge
|
|
||||||
|
|
||||||
VelocityForgeClientConnectionPhase clientPhase = (VelocityForgeClientConnectionPhase) player.getPhase();
|
|
||||||
|
|
||||||
|
|
||||||
if (!player.isActive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clientPhase.consideredComplete()) {
|
|
||||||
//Initial Forge
|
|
||||||
if (message instanceof ModListPacket modListPacket) {
|
|
||||||
clientPhase.forgeHandshake = new ForgeHandshake();
|
|
||||||
}
|
|
||||||
if (message instanceof RegistryPacket registryPacket) {
|
|
||||||
clientPhase.forgeHandshake.addRegistry(registryPacket);
|
|
||||||
}
|
|
||||||
player.getConnection().write(message);
|
|
||||||
} else {
|
|
||||||
//Reset client if not ready to receive new handshake
|
|
||||||
if (clientPhase.getResetType() == VelocityForgeClientConnectionPhase.ClientResetType.CRP ||
|
|
||||||
clientPhase.getResetType() == VelocityForgeClientConnectionPhase.ClientResetType.SR) {
|
|
||||||
clientPhase.resetConnectionPhase(player);
|
|
||||||
player.getConnection().write(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientPhase.forgeHandshake.getModListReplyPacket() == null) {
|
|
||||||
//We have nothing to respond with during this handshake. Unable to proceed.
|
|
||||||
if (Ambassador.getInstance().config.isEnableKickReset()) {
|
|
||||||
//Kick-reset
|
|
||||||
Ambassador.getInstance().reconnectSwitchPlayer(player);
|
|
||||||
} else {
|
|
||||||
Ambassador.getInstance().logger.error("Unable for {} to switch servers. Vanilla({}) -> Forge({}) switch " +
|
|
||||||
"without client side mod or kick-reset enabled is not yet supported!",
|
|
||||||
player.getGameProfile().getName(), player.getConnectedServer().getServerInfo().getName(),
|
|
||||||
server.getServerInfo().getName());
|
|
||||||
server.disconnect();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message instanceof ModListPacket modListPacket) {
|
|
||||||
remainingRegistries = new CountDownLatch(modListPacket.getRegistries().size());
|
|
||||||
|
|
||||||
if (Ambassador.getInstance().config.isDebugMode())
|
|
||||||
player.sendMessage(Component.text("Expecting " + modListPacket.getRegistries().size() +
|
|
||||||
" packets from server " + server.getServer().getServerInfo().getName()));
|
|
||||||
|
|
||||||
long time = System.currentTimeMillis();
|
|
||||||
CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
|
||||||
remainingRegistries.await();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}).thenAcceptAsync((v) -> {
|
|
||||||
|
|
||||||
if(Ambassador.getInstance().config.isDebugMode()) {
|
|
||||||
player.sendMessage(Component.text("Handshake took: " + (System.currentTimeMillis()-time) + " ms"));
|
|
||||||
player.sendMessage(Component.text("Avg packet time" +
|
|
||||||
(System.currentTimeMillis()-time)/modListPacket.getRegistries().size() + " ms"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Ambassador.getInstance().config.isBypassRegistryCheck() ||
|
|
||||||
clientPhase.forgeHandshake.isCompatible(handshake)) {
|
|
||||||
server.ensureConnected().write(clientPhase.forgeHandshake.getModListReplyPacket());
|
|
||||||
} else if (Ambassador.getInstance().config.isEnableKickReset()) {
|
|
||||||
//Kick-reset
|
|
||||||
Ambassador.getInstance().reconnectSwitchPlayer(player);
|
|
||||||
} else {
|
|
||||||
Ambassador.getInstance().logger.error("Unable to switch due to the registries of " +
|
|
||||||
server.getServer().getServerInfo().getName() + " being different from the registries of " +
|
|
||||||
player.getConnectedServer().getServer().getServerInfo().getName());
|
|
||||||
server.disconnect();
|
|
||||||
}
|
|
||||||
}, server.ensureConnected().eventLoop());
|
|
||||||
} else if (message instanceof RegistryPacket registryPacket) {
|
|
||||||
server.getConnection().write(new ACKPacket(Context.fromContext(message.getContext(), true)));
|
|
||||||
handshake.addRegistry(registryPacket);
|
|
||||||
remainingRegistries.countDown();
|
|
||||||
} else if (message instanceof ConfigDataPacket) {
|
|
||||||
server.getConnection().write(new ACKPacket(Context.fromContext(message.getContext(), true)));
|
|
||||||
} else if (message instanceof GenericForgeLoginWrapperPacket<Context> packet
|
|
||||||
&& ForgeHandshakeUtils.ThirdPartyRegistryUtils.isThirdPartyPacket(packet)) {
|
|
||||||
server.getConnection().write(
|
|
||||||
ForgeHandshakeUtils.ThirdPartyRegistryUtils.getThirdPartyChannel(packet).
|
|
||||||
generateResponsePacket(
|
|
||||||
Context.ClientContext.fromContext(packet.getContext(), true),
|
|
||||||
clientPhase.forgeHandshake));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Forge server
|
|
||||||
//To avoid unnecessary resets, we wait until we get the handshake even if we know that we should
|
|
||||||
//reset because that the previous server was Forge.
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onLoginSuccess(VelocityServerConnection serverCon, ConnectedPlayer player) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTransitionToNewPhase(VelocityServerConnection connection) {
|
|
||||||
}
|
|
||||||
|
|
||||||
VelocityForgeBackendConnectionPhase nextPhase() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VelocityForgeBackendConnectionPhase getNewPhase(VelocityServerConnection serverConnection,
|
|
||||||
IForgeLoginWrapperPacket<Context> packet) {
|
|
||||||
VelocityForgeBackendConnectionPhase phaseToTransitionTo = nextPhase();
|
|
||||||
if (phaseToTransitionTo != this) {
|
|
||||||
phaseToTransitionTo.onTransitionToNewPhase(serverConnection);
|
|
||||||
}
|
|
||||||
return phaseToTransitionTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(VelocityServerConnection server, ConnectedPlayer player, PluginMessagePacket message) {
|
|
||||||
if (message.getChannel().equals("ambassador:commands")) {
|
|
||||||
AvailableCommandsPacket packet = new AvailableCommandsPacket();
|
|
||||||
packet.decode(message.content(), ProtocolUtils.Direction.CLIENTBOUND,server.getConnection().getProtocolVersion());
|
|
||||||
server.getConnection().getActiveSessionHandler().handle(packet);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean consideredComplete() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,267 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ClientConnectionPhase;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessagePacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import org.adde0109.ambassador.Ambassador;
|
|
||||||
import org.adde0109.ambassador.forge.packet.Context;
|
|
||||||
import org.adde0109.ambassador.forge.packet.GenericForgeLoginWrapperPacket;
|
|
||||||
import org.adde0109.ambassador.forge.packet.IForgeLoginWrapperPacket;
|
|
||||||
import org.adde0109.ambassador.forge.packet.ModListReplyPacket;
|
|
||||||
import org.adde0109.ambassador.velocity.client.FML2CRPMResetCompleteDecoder;
|
|
||||||
import org.adde0109.ambassador.velocity.client.OutboundSuccessHolder;
|
|
||||||
import org.adde0109.ambassador.velocity.client.ClientPacketQueue;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public enum VelocityForgeClientConnectionPhase implements ClientConnectionPhase {
|
|
||||||
|
|
||||||
NOT_STARTED {
|
|
||||||
@Override
|
|
||||||
VelocityForgeClientConnectionPhase nextPhase() {
|
|
||||||
return IN_PROGRESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void complete(ConnectedPlayer player) {
|
|
||||||
//When no handshake has taken place.
|
|
||||||
//Test if the client supports CRP.
|
|
||||||
ClientResetType.CRP.doReset(player);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
IN_PROGRESS {
|
|
||||||
},
|
|
||||||
WAITING_RESET() {
|
|
||||||
@Override
|
|
||||||
void onTransitionToNewPhase(ConnectedPlayer player) {
|
|
||||||
//We unregister so no plugin sees this client while the client is being reset.
|
|
||||||
((VelocityServer) Ambassador.getInstance().server).unregisterConnection(player);
|
|
||||||
player.getConnection().getChannel().pipeline().addAfter(Connections.MINECRAFT_ENCODER,
|
|
||||||
ForgeConstants.LOGIN_PACKET_QUEUE, new ClientPacketQueue(StateRegistry.PLAY));
|
|
||||||
if (player.getConnection().getChannel().pipeline().get(ForgeConstants.PLUGIN_PACKET_QUEUE) == null)
|
|
||||||
player.getConnection().getChannel().pipeline().addAfter(Connections.MINECRAFT_ENCODER,
|
|
||||||
ForgeConstants.PLUGIN_PACKET_QUEUE, new ClientPacketQueue(StateRegistry.LOGIN));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(ConnectedPlayer player, IForgeLoginWrapperPacket msg, VelocityServerConnection server) {
|
|
||||||
if (msg.getContext().getResponseID() == 98) {
|
|
||||||
//Reset complete
|
|
||||||
player.getConnection().getChannel().pipeline().remove(ForgeConstants.RESET_LISTENER);
|
|
||||||
player.setPhase(NOT_STARTED);
|
|
||||||
|
|
||||||
player.getConnection().getChannel().pipeline().remove(ForgeConstants.LOGIN_PACKET_QUEUE);
|
|
||||||
|
|
||||||
if (!(server.getConnection().getType() instanceof ForgeFMLConnectionType)) {
|
|
||||||
// -> vanilla
|
|
||||||
complete(player, ((Context.ClientContext) msg.getContext()).success() ? ClientResetType.CRP : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player.getConnectionInFlight() != null) {
|
|
||||||
player.getConnectionInFlight().getConnection().getChannel().config().setAutoRead(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
COMPLETE {
|
|
||||||
|
|
||||||
private ClientResetType resetType = ClientResetType.UNKNOWN;
|
|
||||||
@Override
|
|
||||||
void onTransitionToNewPhase(ConnectedPlayer player) {
|
|
||||||
//Send Login Success to client
|
|
||||||
MinecraftConnection connection = player.getConnection();
|
|
||||||
((OutboundSuccessHolder) connection.getChannel().pipeline().get(ForgeConstants.SERVER_SUCCESS_LISTENER))
|
|
||||||
.sendPacket();
|
|
||||||
connection.setState(StateRegistry.PLAY);
|
|
||||||
//Plugins may now send packets to client
|
|
||||||
player.getConnection().getChannel().pipeline().remove(ForgeConstants.PLUGIN_PACKET_QUEUE);
|
|
||||||
((VelocityServer) Ambassador.getInstance().server).registerConnection(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void resetConnectionPhase(ConnectedPlayer player) {
|
|
||||||
getResetType().doReset(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean consideredComplete() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void complete(ConnectedPlayer player) {
|
|
||||||
if (Ambassador.getInstance().config.isDebugMode()) {
|
|
||||||
player.sendMessage(Component.text("Not resetting"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void setResetType(ConnectedPlayer player, ClientResetType resetType) {
|
|
||||||
this.resetType = resetType;
|
|
||||||
if (Ambassador.getInstance().config.isDebugMode()) {
|
|
||||||
player.sendMessage(Component.text("Reset type: " + this.resetType.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClientResetType getResetType() {
|
|
||||||
return resetType;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//TODO: Make a new class that's linked to each player with these fields instead of having them in this phase class
|
|
||||||
public ForgeHandshake forgeHandshake = new ForgeHandshake();
|
|
||||||
|
|
||||||
public boolean handle(ConnectedPlayer player, IForgeLoginWrapperPacket<Context.ClientContext> msg, VelocityServerConnection server) {
|
|
||||||
|
|
||||||
if (msg.getContext().getChannelName().equals("zeta:main")) {
|
|
||||||
forgeHandshake.zetaFlagsPacket = (GenericForgeLoginWrapperPacket<Context.ClientContext>) msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg instanceof ModListReplyPacket replyPacket) {
|
|
||||||
ModInfo modInfo = new ModInfo("FML2", replyPacket.getMods().stream().map(
|
|
||||||
(v) -> new ModInfo.Mod(v,"1")).toList());
|
|
||||||
player.setModInfo(modInfo);
|
|
||||||
forgeHandshake.setModListReplyPacket(replyPacket);
|
|
||||||
if (!(server.getConnection().getType() instanceof ForgeFMLConnectionType)) {
|
|
||||||
complete(player);
|
|
||||||
player.getConnectionInFlight().getConnection().getChannel().config().setAutoRead(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
replyPacket.getChannels().put(MinecraftChannelIdentifier.from("ambassador:commands"),"1");
|
|
||||||
}
|
|
||||||
|
|
||||||
player.getConnectionInFlight().getConnection().write(msg);
|
|
||||||
|
|
||||||
player.setPhase(nextPhase());
|
|
||||||
nextPhase().forgeHandshake = this.forgeHandshake;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
public void complete(ConnectedPlayer player) {
|
|
||||||
complete(player, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void complete(ConnectedPlayer player, ClientResetType resetType) {
|
|
||||||
//Change phase to COMPLETE
|
|
||||||
player.setPhase(COMPLETE);
|
|
||||||
COMPLETE.onTransitionToNewPhase(player);
|
|
||||||
COMPLETE.forgeHandshake = forgeHandshake;
|
|
||||||
if (resetType != null) {
|
|
||||||
COMPLETE.setResetType(player, resetType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Ambassador.getInstance().config.isDebugMode()) {
|
|
||||||
player.sendMessage(Component.text("Forge handshake complete"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTransitionToNewPhase(ConnectedPlayer player) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
VelocityForgeClientConnectionPhase nextPhase() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean consideredComplete() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientResetType getResetType() {
|
|
||||||
return COMPLETE.getResetType();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientResetType getResetType(ConnectedPlayer player) {
|
|
||||||
if (Ambassador.getInstance().config.isDebugMode()) {
|
|
||||||
player.sendMessage(Component.text("Scanning modlist for client reset mods"));
|
|
||||||
}
|
|
||||||
if (player.getModInfo().isPresent()) {
|
|
||||||
if (player.getModInfo().get().getMods().stream().anyMatch((mod -> mod.getId().equals("clientresetpacket")))) {
|
|
||||||
return ClientResetType.CRP;
|
|
||||||
} else if (Ambassador.getInstance().config.getServerSwitchCancellationTime() >= 0 &&
|
|
||||||
player.getModInfo().get().getMods().stream().anyMatch((mod -> mod.getId().equals("serverredirect")
|
|
||||||
|| mod.getId().equals("srvredirect:red")))
|
|
||||||
&& player.getVirtualHost().isPresent()) {
|
|
||||||
return ClientResetType.SR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ClientResetType.NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setResetType(ConnectedPlayer player, ClientResetType resetType) {
|
|
||||||
COMPLETE.setResetType(player, resetType);
|
|
||||||
}
|
|
||||||
public void updateResetType(ConnectedPlayer player) {
|
|
||||||
COMPLETE.setResetType(player, getResetType(player));
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ClientResetType {
|
|
||||||
UNKNOWN,
|
|
||||||
NONE,
|
|
||||||
CRP {
|
|
||||||
@Override
|
|
||||||
void doReset(ConnectedPlayer player) {
|
|
||||||
MinecraftConnection connection = player.getConnection();
|
|
||||||
|
|
||||||
//There is no going back even if the handshake fails. No reason to still be connected.
|
|
||||||
if (player.getConnectedServer() != null) {
|
|
||||||
player.getConnectedServer().disconnect();
|
|
||||||
player.setConnectedServer(null);
|
|
||||||
}
|
|
||||||
//Don't handle anything from the server until the reset has completed.
|
|
||||||
if (player.getConnectionInFlight() != null) {
|
|
||||||
player.getConnectionInFlight().getConnection().getChannel().config().setAutoRead(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connection.getState() == StateRegistry.PLAY || connection.getState() == StateRegistry.CONFIG) {
|
|
||||||
connection.write(new PluginMessagePacket("fml:handshake", Unpooled.wrappedBuffer(ForgeHandshakeUtils.generatePluginResetPacket())));
|
|
||||||
connection.setState(StateRegistry.LOGIN);
|
|
||||||
} else {
|
|
||||||
connection.write(new LoginPluginMessagePacket(98,"fml:loginwrapper", Unpooled.wrappedBuffer(ForgeHandshakeUtils.generateResetPacket())));
|
|
||||||
}
|
|
||||||
|
|
||||||
//Prepare to receive reset ACK
|
|
||||||
connection.getChannel().pipeline().addBefore(Connections.MINECRAFT_DECODER,
|
|
||||||
ForgeConstants.RESET_LISTENER, new FML2CRPMResetCompleteDecoder());
|
|
||||||
|
|
||||||
//Transition
|
|
||||||
player.setPhase(WAITING_RESET);
|
|
||||||
WAITING_RESET.onTransitionToNewPhase(player);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SR {
|
|
||||||
@Override
|
|
||||||
void doReset(ConnectedPlayer player) {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
ProtocolUtils.writeVarInt(buf, 0);
|
|
||||||
buf.writeBytes((player.getVirtualHost().get().getHostName() + ":"
|
|
||||||
+ player.getVirtualHost().get().getPort()).getBytes(StandardCharsets.UTF_8));
|
|
||||||
player.getConnection().write(new PluginMessagePacket("srvredirect:red", buf));
|
|
||||||
|
|
||||||
Ambassador.getInstance().reconnectSwitchPlayer(player);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void doReset(ConnectedPlayer player) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
|
|
||||||
public class ACKPacket implements IForgeLoginWrapperPacket<Context.ClientContext> {
|
|
||||||
|
|
||||||
private final Context.ClientContext context;
|
|
||||||
|
|
||||||
public ACKPacket(Context.ClientContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public ByteBuf encode() {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, 99);
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Context.ClientContext getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
|
|
||||||
public class ConfigDataPacket implements IForgeLoginWrapperPacket<Context> {
|
|
||||||
|
|
||||||
private final String fileName;
|
|
||||||
private final byte[] fileData;
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
public ConfigDataPacket(String fileName, byte[] fileData, Context context) {
|
|
||||||
this.fileName = fileName;
|
|
||||||
this.fileData = fileData;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ConfigDataPacket read(ByteBuf input, Context context, boolean FML3) {
|
|
||||||
String registryName = ProtocolUtils.readString(input);
|
|
||||||
byte[] snapshot = ProtocolUtils.readByteArray(input);
|
|
||||||
|
|
||||||
return new ConfigDataPacket(registryName, snapshot, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuf encode() {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, 4);
|
|
||||||
|
|
||||||
ProtocolUtils.writeString(buf, fileName);
|
|
||||||
ProtocolUtils.writeByteArray(buf, fileData);
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
public class Context {
|
|
||||||
|
|
||||||
private final int responseID;
|
|
||||||
|
|
||||||
private final String channelName;
|
|
||||||
|
|
||||||
private Context(int responseID, String channelName) {
|
|
||||||
this.responseID = responseID;
|
|
||||||
this.channelName = channelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Context createContext(int responseID, String channelName) {
|
|
||||||
return new Context(responseID, channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ClientContext createClientContext(int responseID, boolean clientSuccess, String channelName) {
|
|
||||||
return new ClientContext(responseID, clientSuccess, channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ClientContext fromContext(Context context, boolean clientSuccess) {
|
|
||||||
return new ClientContext(context.responseID, clientSuccess, context.channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getResponseID() {
|
|
||||||
return responseID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getChannelName() {
|
|
||||||
return channelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ClientContext extends Context {
|
|
||||||
|
|
||||||
private final boolean clientSuccess;
|
|
||||||
ClientContext(int responseID, boolean clientSuccess, String channelName) {
|
|
||||||
super(responseID, channelName);
|
|
||||||
this.clientSuccess = clientSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean success() {
|
|
||||||
return clientSuccess;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
|
|
||||||
public class GenericForgeLoginWrapperPacket<T extends Context> implements IForgeLoginWrapperPacket<T> {
|
|
||||||
|
|
||||||
private final byte[] content;
|
|
||||||
private final T context;
|
|
||||||
|
|
||||||
public GenericForgeLoginWrapperPacket(byte[] content, T context) {
|
|
||||||
this.content = content;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static public GenericForgeLoginWrapperPacket<?> read(ByteBuf input, Context context) {
|
|
||||||
byte[] content = new byte[input.readableBytes()];
|
|
||||||
input.readBytes(content);
|
|
||||||
return new GenericForgeLoginWrapperPacket<>(content, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuf encode() {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
buf.writeBytes(content);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public T getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
|
|
||||||
public interface IForgeLoginWrapperPacket<T extends Context> {
|
|
||||||
ByteBuf encode();
|
|
||||||
T getContext();
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
|
|
||||||
public class ModDataPacket implements IForgeLoginWrapperPacket<Context> {
|
|
||||||
private final byte[] content;
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
ModDataPacket(byte[] content, Context context) {
|
|
||||||
this.content = content;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static public ModDataPacket read(ByteBuf input, Context context) {
|
|
||||||
byte[] content = new byte[input.readableBytes()];
|
|
||||||
input.readBytes(content);
|
|
||||||
return new ModDataPacket(content, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuf encode() {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
ProtocolUtils.writeVarInt(buf, 5); //PacketID
|
|
||||||
buf.writeBytes(content);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
|
||||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ModListPacket implements IForgeLoginWrapperPacket<Context> {
|
|
||||||
private List<String> mods;
|
|
||||||
private Map<ChannelIdentifier, String> channels;
|
|
||||||
private List<String> registries;
|
|
||||||
private final List<String> dataPackRegistries;
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private ModListPacket(List<String> mods, Map<ChannelIdentifier,
|
|
||||||
String> channels, List<String> registries, Context context, List<String> dataPackRegistries) {
|
|
||||||
this.mods = mods;
|
|
||||||
this.channels = channels;
|
|
||||||
this.registries = registries;
|
|
||||||
this.context = context;
|
|
||||||
this.dataPackRegistries = dataPackRegistries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ModListPacket read(ByteBuf input, Context context, boolean FML3) {
|
|
||||||
|
|
||||||
List<String> mods = new ArrayList<>();
|
|
||||||
int len = ProtocolUtils.readVarInt(input);
|
|
||||||
for (int x = 0; x < len; x++)
|
|
||||||
mods.add(ProtocolUtils.readString(input, 0x100));
|
|
||||||
|
|
||||||
Map<ChannelIdentifier, String> channels = new HashMap<>();
|
|
||||||
len = ProtocolUtils.readVarInt(input);
|
|
||||||
for (int x = 0; x < len; x++)
|
|
||||||
channels.put(MinecraftChannelIdentifier.from(ProtocolUtils.readString(input, 32767)),
|
|
||||||
ProtocolUtils.readString(input, 0x100));
|
|
||||||
|
|
||||||
List<String> registries = new ArrayList<>();
|
|
||||||
len = ProtocolUtils.readVarInt(input);
|
|
||||||
for (int x = 0; x < len; x++)
|
|
||||||
registries.add(ProtocolUtils.readString(input, 32767));
|
|
||||||
|
|
||||||
List<String> dataPackRegistries = null;
|
|
||||||
if (FML3 && input.isReadable()) {
|
|
||||||
dataPackRegistries = new ArrayList<>();
|
|
||||||
len = ProtocolUtils.readVarInt(input);
|
|
||||||
for (int x = 0; x < len; x++)
|
|
||||||
dataPackRegistries.add(ProtocolUtils.readString(input, 0x100));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ModListPacket(mods, channels, registries, context, dataPackRegistries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuf encode() {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, 1);
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, mods.size());
|
|
||||||
mods.forEach(m -> ProtocolUtils.writeString(buf, m));
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, channels.size());
|
|
||||||
channels.forEach((k, v) -> {
|
|
||||||
ProtocolUtils.writeString(buf,k.getId());
|
|
||||||
ProtocolUtils.writeString(buf,v);
|
|
||||||
});
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, registries.size());
|
|
||||||
registries.forEach(k -> ProtocolUtils.writeString(buf, k));
|
|
||||||
|
|
||||||
if (dataPackRegistries != null) {
|
|
||||||
ProtocolUtils.writeVarInt(buf, dataPackRegistries.size());
|
|
||||||
dataPackRegistries.forEach(k -> ProtocolUtils.writeString(buf, k));
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getMods() {
|
|
||||||
return mods;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<ChannelIdentifier, String> getChannels() {
|
|
||||||
return channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getRegistries() {
|
|
||||||
return registries;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
|
||||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ModListReplyPacket implements IForgeLoginWrapperPacket<Context.ClientContext> {
|
|
||||||
|
|
||||||
private List<String> mods;
|
|
||||||
private Map<ChannelIdentifier, String> channels;
|
|
||||||
private Map<String, String> registries;
|
|
||||||
|
|
||||||
private final Context.ClientContext context;
|
|
||||||
private ModListReplyPacket(List<String> mods, Map<ChannelIdentifier,
|
|
||||||
String> channels, Map<String, String> registries, Context.ClientContext context) {
|
|
||||||
this.mods = mods;
|
|
||||||
this.channels = channels;
|
|
||||||
this.registries = registries;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ModListReplyPacket read(ByteBuf input, Context.ClientContext context) {
|
|
||||||
|
|
||||||
List<String> mods = new ArrayList<>();
|
|
||||||
int len = ProtocolUtils.readVarInt(input);
|
|
||||||
for (int x = 0; x < len; x++)
|
|
||||||
mods.add(ProtocolUtils.readString(input, 0x100));
|
|
||||||
|
|
||||||
Map<ChannelIdentifier, String> channels = new HashMap<>();
|
|
||||||
len = ProtocolUtils.readVarInt(input);
|
|
||||||
for (int x = 0; x < len; x++)
|
|
||||||
channels.put(MinecraftChannelIdentifier.from(ProtocolUtils.readString(input, 32767)),
|
|
||||||
ProtocolUtils.readString(input, 0x100));
|
|
||||||
|
|
||||||
Map<String, String> registries = new HashMap<>();
|
|
||||||
len = ProtocolUtils.readVarInt(input);
|
|
||||||
for (int x = 0; x < len; x++)
|
|
||||||
registries.put(ProtocolUtils.readString(input, 32767), ProtocolUtils.readString(input, 0x100));
|
|
||||||
|
|
||||||
return new ModListReplyPacket(mods, channels, registries, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuf encode() {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, 2);
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, mods.size());
|
|
||||||
mods.forEach(m -> ProtocolUtils.writeString(buf, m));
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, channels.size());
|
|
||||||
channels.forEach((k, v) -> {
|
|
||||||
ProtocolUtils.writeString(buf,k.getId());
|
|
||||||
ProtocolUtils.writeString(buf,v);
|
|
||||||
});
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, registries.size());
|
|
||||||
registries.forEach((k, v) -> {
|
|
||||||
ProtocolUtils.writeString(buf, k);
|
|
||||||
ProtocolUtils.writeString(buf, v);
|
|
||||||
});
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Context.ClientContext getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getMods() {
|
|
||||||
return mods;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<ChannelIdentifier, String> getChannels() {
|
|
||||||
return channels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.packet;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
|
|
||||||
public class RegistryPacket implements IForgeLoginWrapperPacket<Context> {
|
|
||||||
|
|
||||||
private final String registryName;
|
|
||||||
|
|
||||||
private final byte[] snapshot;
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
public RegistryPacket(String registryName, byte[] snapshot, Context context) {
|
|
||||||
this.registryName = registryName;
|
|
||||||
this.snapshot = snapshot;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RegistryPacket read(ByteBuf input, Context context, boolean FML3) {
|
|
||||||
String registryName = ProtocolUtils.readString(input);
|
|
||||||
byte[] snapshot = null;
|
|
||||||
if (input.readBoolean()) {
|
|
||||||
snapshot = new byte[input.readableBytes()];
|
|
||||||
input.readBytes(snapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RegistryPacket(registryName, snapshot, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuf encode() {
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
|
|
||||||
ProtocolUtils.writeVarInt(buf, 3);
|
|
||||||
|
|
||||||
ProtocolUtils.writeString(buf, registryName);
|
|
||||||
if (snapshot != null) {
|
|
||||||
buf.writeBoolean(true);
|
|
||||||
buf.writeBytes(snapshot);
|
|
||||||
} else {
|
|
||||||
buf.writeBoolean(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRegistryName() {
|
|
||||||
return registryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getSnapshot() {
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.pipeline;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket;
|
|
||||||
import com.velocitypowered.proxy.util.except.QuietRuntimeException;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
|
||||||
import io.netty.handler.codec.CorruptedFrameException;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
|
||||||
import org.adde0109.ambassador.Ambassador;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public class CommandDecoderErrorCatcher extends ChannelInboundHandlerAdapter {
|
|
||||||
|
|
||||||
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
|
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
|
||||||
private boolean sentWarning = false;
|
|
||||||
|
|
||||||
public CommandDecoderErrorCatcher(ProtocolVersion protocolVersion, ConnectedPlayer player) {
|
|
||||||
this.registry = StateRegistry.PLAY.getProtocolRegistry(ProtocolUtils.Direction.CLIENTBOUND, protocolVersion);
|
|
||||||
this.player = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object msg) throws Exception {
|
|
||||||
if (msg instanceof ByteBuf buf) {
|
|
||||||
if (!ctx.channel().isActive() || !buf.isReadable()) {
|
|
||||||
buf.release();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int originalReaderIndex = buf.readerIndex();
|
|
||||||
int packetId = ProtocolUtils.readVarInt(buf);
|
|
||||||
MinecraftPacket packet = registry.createPacket(packetId);
|
|
||||||
buf.readerIndex(originalReaderIndex);
|
|
||||||
if (packet instanceof AvailableCommandsPacket) {
|
|
||||||
try {
|
|
||||||
((MinecraftDecoder) ctx.pipeline().get(Connections.MINECRAFT_DECODER)).channelRead(ctx, msg);
|
|
||||||
} catch (QuietRuntimeException | CorruptedFrameException e) {
|
|
||||||
RegisteredServer server = player.getConnectedServer().getServer();
|
|
||||||
if (!Ambassador.getInstance().config.isSilenceWarnings() && !sentWarning) {
|
|
||||||
player.sendMessage(Component.text("[Ambassador Warning]: Unsupported command argument type detected! " +
|
|
||||||
"Please install Proxy-Compatible-Forge mod on this backend server to have access to commands. " +
|
|
||||||
"This message can be silenced in the ambassador.toml config file.", NamedTextColor.YELLOW));
|
|
||||||
sentWarning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
ctx.fireChannelRead(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.pipeline;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessagePacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponsePacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.handler.codec.DecoderException;
|
|
||||||
import io.netty.handler.codec.MessageToMessageCodec;
|
|
||||||
import io.netty.util.ReferenceCountUtil;
|
|
||||||
import org.adde0109.ambassador.Ambassador;
|
|
||||||
import org.adde0109.ambassador.forge.packet.*;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ForgeLoginWrapperCodec extends MessageToMessageCodec<DeferredByteBufHolder, IForgeLoginWrapperPacket<?>> {
|
|
||||||
|
|
||||||
private final boolean FML3;
|
|
||||||
private final Map<Integer, Context> loginWrapperContexts = new HashMap<>();
|
|
||||||
|
|
||||||
public ForgeLoginWrapperCodec(boolean fml3) {
|
|
||||||
FML3 = fml3;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean acceptInboundMessage(Object msg) throws Exception {
|
|
||||||
return (msg instanceof LoginPluginMessagePacket
|
|
||||||
&& ((LoginPluginMessagePacket) msg).getChannel().equals("fml:loginwrapper"))
|
|
||||||
|| (msg instanceof LoginPluginResponsePacket
|
|
||||||
&& loginWrapperContexts.containsKey(((LoginPluginResponsePacket) msg).getId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void decode(ChannelHandlerContext ctx, DeferredByteBufHolder in, List<Object> out) throws Exception {
|
|
||||||
|
|
||||||
ByteBuf buf = in.content();
|
|
||||||
|
|
||||||
Context context;
|
|
||||||
if (in instanceof LoginPluginResponsePacket msg) {
|
|
||||||
//Continue from stored context
|
|
||||||
context = Context.fromContext(
|
|
||||||
loginWrapperContexts.remove(((LoginPluginResponsePacket) msg).getId()), msg.isSuccess());
|
|
||||||
if (!msg.isSuccess()) {
|
|
||||||
//Nothing to read, just create an empty packet.
|
|
||||||
out.add(GenericForgeLoginWrapperPacket.read(buf, context));
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
String channel = ProtocolUtils.readString(buf); //Read the channel even though we know the channel by context.
|
|
||||||
Ambassador.getInstance();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//New context.
|
|
||||||
LoginPluginMessagePacket msg = (LoginPluginMessagePacket) in;
|
|
||||||
String channel = ProtocolUtils.readString(buf);
|
|
||||||
|
|
||||||
context = Context.createContext(msg.getId(), channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Decoding of data starts here - channel already read
|
|
||||||
int originalReaderIndex = buf.readerIndex();
|
|
||||||
|
|
||||||
if (!context.getChannelName().equals("fml:handshake")) {
|
|
||||||
out.add(GenericForgeLoginWrapperPacket.read(buf, context));
|
|
||||||
} else {
|
|
||||||
int length = ProtocolUtils.readVarInt(buf);
|
|
||||||
int packetID = ProtocolUtils.readVarInt(buf);
|
|
||||||
if (context instanceof Context.ClientContext clientContext) {
|
|
||||||
switch (packetID) {
|
|
||||||
case 2:
|
|
||||||
out.add(ModListReplyPacket.read(buf, clientContext));
|
|
||||||
break;
|
|
||||||
case 99:
|
|
||||||
out.add(new ACKPacket(clientContext));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
//Undo decoding
|
|
||||||
buf.readerIndex(originalReaderIndex);
|
|
||||||
out.add(GenericForgeLoginWrapperPacket.read(buf, context));
|
|
||||||
if (Ambassador.getInstance().config.isDebugMode()) {
|
|
||||||
Ambassador.getInstance().logger.warn(
|
|
||||||
"Unrecognised packet id received from client on fml:handshake: " + packetID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (packetID) {
|
|
||||||
case 1:
|
|
||||||
out.add(ModListPacket.read(buf, context, FML3));
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
out.add(RegistryPacket.read(buf, context, FML3));
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
out.add(ConfigDataPacket.read(buf, context, FML3));
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
if (FML3) {
|
|
||||||
out.add(ModDataPacket.read(buf, context));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
//Undo decoding
|
|
||||||
buf.readerIndex(originalReaderIndex);
|
|
||||||
out.add(GenericForgeLoginWrapperPacket.read(buf, context));
|
|
||||||
if (Ambassador.getInstance().config.isDebugMode()) {
|
|
||||||
Ambassador.getInstance().logger.warn(
|
|
||||||
"Unrecognised packet id received from server on fml:handshake: " + packetID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void encode(ChannelHandlerContext ctx, IForgeLoginWrapperPacket<?> msg, List<Object> out) throws Exception {
|
|
||||||
ByteBuf wrapped;
|
|
||||||
|
|
||||||
boolean data = !(msg.getContext() instanceof Context.ClientContext clientContext && !clientContext.success());
|
|
||||||
|
|
||||||
boolean includeLength = !(msg instanceof GenericForgeLoginWrapperPacket);
|
|
||||||
|
|
||||||
String channel = msg.getContext().getChannelName();
|
|
||||||
|
|
||||||
wrapped = Unpooled.buffer();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
ByteBuf encoded = msg.encode();
|
|
||||||
ProtocolUtils.writeString(wrapped, channel);
|
|
||||||
if (includeLength)
|
|
||||||
ProtocolUtils.writeVarInt(wrapped, encoded.readableBytes());
|
|
||||||
wrapped.writeBytes(encoded);
|
|
||||||
encoded.release();
|
|
||||||
}
|
|
||||||
if (msg.getContext() instanceof Context.ClientContext clientContext) {
|
|
||||||
out.add(new LoginPluginResponsePacket(clientContext.getResponseID(), clientContext.success(), wrapped));
|
|
||||||
} else {
|
|
||||||
out.add(new LoginPluginMessagePacket(msg.getContext().getResponseID(), "fml:loginwrapper", wrapped));
|
|
||||||
if (!(msg instanceof ModDataPacket)) { //ModDataPacket doesn't require a response
|
|
||||||
this.loginWrapperContexts.put(msg.getContext().getResponseID(), msg.getContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package org.adde0109.ambassador.forge.pipeline;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.channel.SimpleChannelInboundHandler;
|
|
||||||
import io.netty.util.ReferenceCountUtil;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeConstants;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeFMLConnectionType;
|
|
||||||
import org.adde0109.ambassador.forge.VelocityForgeBackendConnectionPhase;
|
|
||||||
import org.adde0109.ambassador.forge.packet.Context;
|
|
||||||
import org.adde0109.ambassador.forge.packet.IForgeLoginWrapperPacket;
|
|
||||||
import org.adde0109.ambassador.forge.VelocityForgeClientConnectionPhase;
|
|
||||||
|
|
||||||
public class ForgeLoginWrapperHandler extends SimpleChannelInboundHandler<IForgeLoginWrapperPacket<?>> {
|
|
||||||
|
|
||||||
private final MinecraftConnectionAssociation connection;
|
|
||||||
|
|
||||||
|
|
||||||
public ForgeLoginWrapperHandler(MinecraftConnectionAssociation connection) {
|
|
||||||
super(false);
|
|
||||||
this.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void channelRead0(ChannelHandlerContext ctx, IForgeLoginWrapperPacket msg) throws Exception {
|
|
||||||
if (connection instanceof ConnectedPlayer player) {
|
|
||||||
VelocityForgeClientConnectionPhase phase = (VelocityForgeClientConnectionPhase) player.getPhase();
|
|
||||||
phase.handle(player,msg,player.getConnectionInFlight());
|
|
||||||
} else if (connection instanceof VelocityServerConnection serverConnection){
|
|
||||||
if (!(serverConnection.getConnection().getType() instanceof ForgeFMLConnectionType)) {
|
|
||||||
serverConnection.getConnection().setType(serverConnection.getPlayer().getConnection().getType());
|
|
||||||
serverConnection.setConnectionPhase(serverConnection.getConnection().getType().getInitialBackendPhase());
|
|
||||||
}
|
|
||||||
|
|
||||||
VelocityForgeBackendConnectionPhase phase =
|
|
||||||
(VelocityForgeBackendConnectionPhase) serverConnection.getPhase();
|
|
||||||
phase.handle(serverConnection, serverConnection.getPlayer(), msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.network.BackendChannelInitializer;
|
|
||||||
import io.netty.channel.Channel;
|
|
||||||
import io.netty.channel.ChannelInitializer;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeConstants;
|
|
||||||
import org.adde0109.ambassador.velocity.backend.FMLMarkerAdder;
|
|
||||||
import org.adde0109.ambassador.velocity.backend.VelocityForgeBackendHandshakeHandler;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class VelocityBackendChannelInitializer extends BackendChannelInitializer {
|
|
||||||
|
|
||||||
private final Method INIT_CHANNEL;
|
|
||||||
|
|
||||||
private final ChannelInitializer<Channel> delegate;
|
|
||||||
private final VelocityServer server;
|
|
||||||
|
|
||||||
public VelocityBackendChannelInitializer(ChannelInitializer<Channel> delegate, VelocityServer server) {
|
|
||||||
super(server);
|
|
||||||
this.delegate = delegate;
|
|
||||||
this.server = server;
|
|
||||||
try {
|
|
||||||
INIT_CHANNEL = delegate.getClass().getDeclaredMethod("initChannel", Channel.class);
|
|
||||||
INIT_CHANNEL.setAccessible(true);
|
|
||||||
} catch (ReflectiveOperationException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initChannel(Channel ch) {
|
|
||||||
try {
|
|
||||||
INIT_CHANNEL.invoke(delegate, ch);
|
|
||||||
} catch (ReflectiveOperationException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
ch.pipeline().addLast(ForgeConstants.MARKER_ADDER, new FMLMarkerAdder(server));
|
|
||||||
ch.pipeline().addLast(ForgeConstants.HANDLER, new VelocityForgeBackendHandshakeHandler(server));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.event.Continuation;
|
|
||||||
import com.velocitypowered.api.event.PostOrder;
|
|
||||||
import com.velocitypowered.api.event.Subscribe;
|
|
||||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
|
||||||
import com.velocitypowered.api.event.player.*;
|
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import org.adde0109.ambassador.Ambassador;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeConstants;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeFMLConnectionType;
|
|
||||||
import org.adde0109.ambassador.forge.VelocityForgeClientConnectionPhase;
|
|
||||||
import org.adde0109.ambassador.forge.pipeline.ForgeLoginWrapperCodec;
|
|
||||||
import org.adde0109.ambassador.forge.pipeline.ForgeLoginWrapperHandler;
|
|
||||||
|
|
||||||
public class VelocityEventHandler {
|
|
||||||
|
|
||||||
private final Ambassador ambassador;
|
|
||||||
|
|
||||||
public VelocityEventHandler(Ambassador ambassador) {
|
|
||||||
this.ambassador = ambassador;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe(order = PostOrder.FIRST)
|
|
||||||
public void onPostLoginEvent(PostLoginEvent event, Continuation continuation) {
|
|
||||||
ConnectedPlayer player = (ConnectedPlayer) event.getPlayer();
|
|
||||||
if (player.getPhase() instanceof VelocityForgeClientConnectionPhase) {
|
|
||||||
((VelocityServer) Ambassador.getInstance().server).unregisterConnection(player);
|
|
||||||
|
|
||||||
player.getConnection().eventLoop().submit(() -> {
|
|
||||||
player.getConnection().setState(StateRegistry.LOGIN);
|
|
||||||
|
|
||||||
player.getConnection().getChannel().pipeline().addBefore(
|
|
||||||
Connections.HANDLER,
|
|
||||||
ForgeConstants.FORGE_HANDSHAKE_DECODER, new ForgeLoginWrapperCodec(
|
|
||||||
player.getConnection().getType() == ForgeConstants.ForgeFML3));
|
|
||||||
player.getConnection().getChannel().pipeline().addAfter(
|
|
||||||
ForgeConstants.FORGE_HANDSHAKE_DECODER,
|
|
||||||
ForgeConstants.FORGE_HANDSHAKE_HANDLER, new ForgeLoginWrapperHandler(player));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//event.getPlayer().sendMessage(Component.text("post login event"));
|
|
||||||
continuation.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe(order = PostOrder.LAST)
|
|
||||||
public void onPlayerChooseInitialServerEvent(PlayerChooseInitialServerEvent event, Continuation continuation) {
|
|
||||||
ConnectedPlayer player = (ConnectedPlayer) event.getPlayer();
|
|
||||||
if (!(player.getPhase() instanceof VelocityForgeClientConnectionPhase phase)) {
|
|
||||||
continuation.resume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
RegisteredServer chosenServer = Ambassador.getTemporaryForced().remove(player.getUsername());
|
|
||||||
if (chosenServer != null)
|
|
||||||
event.setInitialServer(chosenServer);
|
|
||||||
//event.getPlayer().sendMessage(Component.text("choose server event"));
|
|
||||||
continuation.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void onPlayerChannelRegisterEvent(PlayerChannelRegisterEvent event) {
|
|
||||||
ConnectedPlayer player = (ConnectedPlayer) event.getPlayer();
|
|
||||||
if (!(player.getConnection().getType() instanceof ForgeFMLConnectionType)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
player.setModInfo(new ModInfo("Channels", event.getChannels().stream().map((id) -> {
|
|
||||||
return new ModInfo.Mod(id.getId(), "");
|
|
||||||
}).toList()));
|
|
||||||
|
|
||||||
VelocityForgeClientConnectionPhase clientPhase = (VelocityForgeClientConnectionPhase) player.getPhase();
|
|
||||||
//If reset typ is still unknown, set it!
|
|
||||||
if (clientPhase.getResetType() == VelocityForgeClientConnectionPhase.ClientResetType.UNKNOWN) {
|
|
||||||
clientPhase.updateResetType(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.network.ServerChannelInitializer;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import io.netty.channel.Channel;
|
|
||||||
import io.netty.channel.ChannelHandler;
|
|
||||||
import io.netty.channel.ChannelInitializer;
|
|
||||||
import org.adde0109.ambassador.velocity.client.VelocityHandshakeSessionHandler;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class VelocityServerChannelInitializer extends ServerChannelInitializer {
|
|
||||||
private Method INIT_CHANNEL;
|
|
||||||
|
|
||||||
private final ChannelInitializer<?> delegate;
|
|
||||||
private final VelocityServer server;
|
|
||||||
|
|
||||||
public VelocityServerChannelInitializer(ChannelInitializer<?> delegate,VelocityServer server) {
|
|
||||||
super(server);
|
|
||||||
this.delegate = delegate;
|
|
||||||
this.server = server;
|
|
||||||
try {
|
|
||||||
INIT_CHANNEL = delegate.getClass().getDeclaredMethod("initChannel", Channel.class);
|
|
||||||
INIT_CHANNEL.setAccessible(true);
|
|
||||||
} catch (ReflectiveOperationException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initChannel(@NotNull Channel ch){
|
|
||||||
try {
|
|
||||||
INIT_CHANNEL.invoke(delegate,ch);
|
|
||||||
} catch (ReflectiveOperationException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if (ch.pipeline().get(MinecraftConnection.class) == null)
|
|
||||||
super.initChannel(ch);
|
|
||||||
MinecraftConnection handler = ch.pipeline().get(MinecraftConnection.class);
|
|
||||||
HandshakeSessionHandler originalSessionHandler = (HandshakeSessionHandler) handler.getActiveSessionHandler();
|
|
||||||
handler.setActiveSessionHandler(StateRegistry.HANDSHAKE, new VelocityHandshakeSessionHandler(originalSessionHandler, handler, server));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.backend;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
|
||||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
|
|
||||||
import io.netty.channel.*;
|
|
||||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeConstants;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeFMLConnectionType;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN;
|
|
||||||
|
|
||||||
public class FMLMarkerAdder extends MessageToMessageEncoder<HandshakePacket> {
|
|
||||||
|
|
||||||
final VelocityServer server;
|
|
||||||
|
|
||||||
public FMLMarkerAdder(VelocityServer server) {
|
|
||||||
super(HandshakePacket.class);
|
|
||||||
this.server = server;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void encode(ChannelHandlerContext ctx, HandshakePacket msg, List<Object> out) {
|
|
||||||
MinecraftConnection connection = (MinecraftConnection) ctx.pipeline().get(Connections.HANDLER);
|
|
||||||
VelocityServerConnection serverConnection = (VelocityServerConnection) connection.getAssociation();
|
|
||||||
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
|
|
||||||
|
|
||||||
if (serverConnection.getPlayer().getConnection().getType() instanceof ForgeFMLConnectionType FMLType
|
|
||||||
&& forwardingMode != PlayerInfoForwarding.LEGACY
|
|
||||||
&& forwardingMode != PlayerInfoForwarding.BUNGEEGUARD) {
|
|
||||||
msg.setServerAddress(msg.getServerAddress() + (FMLType == ForgeConstants.ForgeFML3 ? ForgeConstants.FML3Marker : ForgeConstants.FML2Marker));
|
|
||||||
}
|
|
||||||
out.add(msg);
|
|
||||||
ctx.pipeline().remove(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.backend;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.*;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessagePacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
|
||||||
import com.velocitypowered.proxy.util.except.QuietRuntimeException;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import org.adde0109.ambassador.forge.*;
|
|
||||||
|
|
||||||
public class ForgeLoginSessionHandler implements MinecraftSessionHandler {
|
|
||||||
|
|
||||||
private final LoginSessionHandler original;
|
|
||||||
private final VelocityServerConnection serverConnection;
|
|
||||||
private final VelocityServer server;
|
|
||||||
|
|
||||||
public ForgeLoginSessionHandler(LoginSessionHandler original, VelocityServerConnection serverConnection, VelocityServer server) {
|
|
||||||
this.original = original;
|
|
||||||
this.serverConnection = serverConnection;
|
|
||||||
this.server = server;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(ServerLoginSuccessPacket packet) {
|
|
||||||
if ((serverConnection.getPhase() instanceof VelocityForgeBackendConnectionPhase phase)) {
|
|
||||||
phase.onLoginSuccess(serverConnection,serverConnection.getPlayer());
|
|
||||||
}
|
|
||||||
|
|
||||||
original.handle(packet); //Can lead to disconnect.
|
|
||||||
|
|
||||||
//If we are still connected after handling that package.
|
|
||||||
if (serverConnection.getConnection() != null) {
|
|
||||||
ConnectedPlayer player = serverConnection.getPlayer();
|
|
||||||
|
|
||||||
VelocityForgeClientConnectionPhase clientPhase = (VelocityForgeClientConnectionPhase) player.getPhase();
|
|
||||||
clientPhase.complete(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(DisconnectPacket packet) {
|
|
||||||
return original.handle(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disconnected() {
|
|
||||||
original.disconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleGeneric(MinecraftPacket packet) {
|
|
||||||
if (!packet.handle(original))
|
|
||||||
original.handleGeneric(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MinecraftSessionHandler getOriginal() {
|
|
||||||
return this.original;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.backend;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.LoginSessionHandler;
|
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import io.netty.channel.*;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeConstants;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeFMLConnectionType;
|
|
||||||
import org.adde0109.ambassador.forge.pipeline.ForgeLoginWrapperCodec;
|
|
||||||
import org.adde0109.ambassador.forge.pipeline.ForgeLoginWrapperHandler;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public class VelocityForgeBackendHandshakeHandler extends ChannelInboundHandlerAdapter {
|
|
||||||
|
|
||||||
private final VelocityServer server;
|
|
||||||
|
|
||||||
public VelocityForgeBackendHandshakeHandler(VelocityServer server) {
|
|
||||||
this.server = server;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void channelActive(@NotNull ChannelHandlerContext ctx) throws Exception {
|
|
||||||
MinecraftConnection connection = (MinecraftConnection) ctx.pipeline().get(Connections.HANDLER);
|
|
||||||
VelocityServerConnection serverConnection = (VelocityServerConnection) connection.getAssociation();
|
|
||||||
|
|
||||||
ctx.pipeline().remove(this);
|
|
||||||
|
|
||||||
ConnectedPlayer player = serverConnection.getPlayer();
|
|
||||||
if (player.getConnection().getType() instanceof ForgeFMLConnectionType) {
|
|
||||||
ForgeLoginSessionHandler forgeLoginSessionHandler = new ForgeLoginSessionHandler((LoginSessionHandler) connection.getActiveSessionHandler(), serverConnection,server);
|
|
||||||
connection.setActiveSessionHandler(StateRegistry.LOGIN, forgeLoginSessionHandler);
|
|
||||||
|
|
||||||
serverConnection.getConnection().getChannel().pipeline().addBefore(
|
|
||||||
Connections.HANDLER,
|
|
||||||
ForgeConstants.FORGE_HANDSHAKE_DECODER, new ForgeLoginWrapperCodec(
|
|
||||||
player.getConnection().getType() == ForgeConstants.ForgeFML3));
|
|
||||||
serverConnection.getConnection().getChannel().pipeline().addAfter(
|
|
||||||
ForgeConstants.FORGE_HANDSHAKE_DECODER,
|
|
||||||
ForgeConstants.FORGE_HANDSHAKE_HANDLER, new ForgeLoginWrapperHandler(serverConnection));
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.pipeline().fireChannelActive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.client;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import io.netty.channel.*;
|
|
||||||
|
|
||||||
|
|
||||||
public class ClientPacketQueue extends ChannelOutboundHandlerAdapter {
|
|
||||||
|
|
||||||
private PendingWriteQueue queue;
|
|
||||||
private final StateRegistry allow;
|
|
||||||
|
|
||||||
public ClientPacketQueue(StateRegistry registry) {
|
|
||||||
this.allow = registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
|
||||||
queue = new PendingWriteQueue(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
|
||||||
MinecraftConnection connection = ctx.pipeline().get(MinecraftConnection.class);
|
|
||||||
if (msg instanceof MinecraftPacket packet) {
|
|
||||||
try {
|
|
||||||
allow.getProtocolRegistry(ProtocolUtils.Direction.CLIENTBOUND ,
|
|
||||||
connection.getProtocolVersion()).getPacketId(packet);
|
|
||||||
ctx.write(msg,promise);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
queue.add(msg, promise);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.write(msg,promise);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
|
||||||
if (ctx.channel().isActive()) {
|
|
||||||
queue.removeAndWriteAll();
|
|
||||||
ctx.flush();
|
|
||||||
} else {
|
|
||||||
queue.removeAndFailAll(new ChannelException());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.client;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
|
||||||
import org.adde0109.ambassador.forge.packet.Context;
|
|
||||||
import org.adde0109.ambassador.forge.packet.GenericForgeLoginWrapperPacket;
|
|
||||||
import org.adde0109.ambassador.forge.packet.IForgeLoginWrapperPacket;
|
|
||||||
|
|
||||||
public class FML2CRPMResetCompleteDecoder extends ChannelInboundHandlerAdapter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
|
||||||
if (msg instanceof ByteBuf buf) {
|
|
||||||
if (!ctx.channel().isActive() || !buf.isReadable()) {
|
|
||||||
buf.release();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int originalReaderIndex = buf.readerIndex();
|
|
||||||
int packetId = ProtocolUtils.readVarInt(buf);
|
|
||||||
if (packetId == 0x02 && buf.readableBytes() > 1) {
|
|
||||||
try {
|
|
||||||
int id = ProtocolUtils.readVarInt(buf);
|
|
||||||
boolean success = buf.readBoolean();
|
|
||||||
if (id == 98) {
|
|
||||||
try {
|
|
||||||
ctx.fireChannelRead(GenericForgeLoginWrapperPacket.read(
|
|
||||||
Unpooled.EMPTY_BUFFER, Context.createClientContext(id, success, "fml:handshake")));
|
|
||||||
} finally {
|
|
||||||
buf.release();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
buf.readerIndex(originalReaderIndex);
|
|
||||||
}
|
|
||||||
ctx.fireChannelRead(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.client;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
|
||||||
import io.netty.channel.ChannelPromise;
|
|
||||||
|
|
||||||
public class OutboundSuccessHolder extends ChannelOutboundHandlerAdapter {
|
|
||||||
|
|
||||||
private ServerLoginSuccessPacket packet;
|
|
||||||
private ChannelHandlerContext ctx;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
|
||||||
this.ctx = ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
|
||||||
if ((msg instanceof ServerLoginSuccessPacket packet)) {
|
|
||||||
this.packet = packet;
|
|
||||||
} else {
|
|
||||||
ctx.write(msg, promise);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendPacket() {
|
|
||||||
ctx.write(packet, ctx.voidPromise());
|
|
||||||
ctx.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.client;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|
||||||
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import org.adde0109.ambassador.forge.ForgeConstants;
|
|
||||||
|
|
||||||
public class VelocityHandshakeSessionHandler extends HandshakeSessionHandler {
|
|
||||||
private final HandshakeSessionHandler original;
|
|
||||||
private final MinecraftConnection connection;
|
|
||||||
|
|
||||||
public VelocityHandshakeSessionHandler(HandshakeSessionHandler original, MinecraftConnection connection, VelocityServer server) {
|
|
||||||
super(connection, server);
|
|
||||||
this.original = original;
|
|
||||||
this.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(HandshakePacket handshake) {
|
|
||||||
handshake.handle(original);
|
|
||||||
if (connection.getType() == ConnectionTypes.VANILLA) {
|
|
||||||
final String[] markerSplit = handshake.getServerAddress().split("\0");
|
|
||||||
if (connection.getState() == StateRegistry.LOGIN && markerSplit.length > 1 && markerSplit[1].startsWith("FML")) {
|
|
||||||
switch (markerSplit[1]) {
|
|
||||||
case "FML2":
|
|
||||||
connection.setType(ForgeConstants.ForgeFML2);
|
|
||||||
break;
|
|
||||||
case "FML3":
|
|
||||||
connection.setType(ForgeConstants.ForgeFML3);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
connection.getChannel().pipeline().addAfter(Connections.MINECRAFT_ENCODER,ForgeConstants.SERVER_SUCCESS_LISTENER, new OutboundSuccessHolder());
|
|
||||||
connection.getChannel().pipeline().addAfter(Connections.MINECRAFT_ENCODER,ForgeConstants.PLUGIN_PACKET_QUEUE, new ClientPacketQueue(StateRegistry.LOGIN));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleGeneric(MinecraftPacket packet) {
|
|
||||||
original.handleGeneric(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleUnknown(ByteBuf buf) {
|
|
||||||
original.handleUnknown(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.protocol;
|
|
||||||
|
|
||||||
import com.mojang.brigadier.StringReader;
|
|
||||||
import com.mojang.brigadier.arguments.ArgumentType;
|
|
||||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
|
||||||
|
|
||||||
public class EnumArgumentProperty implements ArgumentType<String> {
|
|
||||||
|
|
||||||
private final String className;
|
|
||||||
|
|
||||||
public EnumArgumentProperty(String className) {
|
|
||||||
this.className = className;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClassName() {
|
|
||||||
return this.className;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String parse(StringReader reader) throws CommandSyntaxException {
|
|
||||||
return reader.readUnquotedString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.protocol;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentPropertySerializer;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An argument property serializer that will serialize and deserialize nothing.
|
|
||||||
*/
|
|
||||||
public class EnumArgumentPropertySerializer implements ArgumentPropertySerializer<EnumArgumentProperty> {
|
|
||||||
|
|
||||||
public static final EnumArgumentPropertySerializer ENUM = new EnumArgumentPropertySerializer();
|
|
||||||
|
|
||||||
private EnumArgumentPropertySerializer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable EnumArgumentProperty deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
|
||||||
return new EnumArgumentProperty(ProtocolUtils.readString(buf));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void serialize(EnumArgumentProperty object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
|
||||||
ProtocolUtils.writeString(buf, object.getClassName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package org.adde0109.ambassador.velocity.protocol;
|
|
||||||
|
|
||||||
import com.mojang.brigadier.StringReader;
|
|
||||||
import com.mojang.brigadier.arguments.ArgumentType;
|
|
||||||
import com.mojang.brigadier.context.CommandContext;
|
|
||||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
|
||||||
import com.mojang.brigadier.suggestion.Suggestions;
|
|
||||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
|
||||||
import com.velocitypowered.api.proxy.Player;
|
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
public class ModIdArgumentProperty implements ArgumentType<String> {
|
|
||||||
|
|
||||||
public ModIdArgumentProperty() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String parse(StringReader reader) throws CommandSyntaxException {
|
|
||||||
return reader.readUnquotedString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context,
|
|
||||||
SuggestionsBuilder builder) {
|
|
||||||
S source = context.getSource();
|
|
||||||
|
|
||||||
if (source instanceof Player) {
|
|
||||||
ModInfo modInfo = ((Player) source).getModInfo().orElse(null);
|
|
||||||
|
|
||||||
if (modInfo != null) {
|
|
||||||
for (ModInfo.Mod mod : modInfo.getMods()) {
|
|
||||||
builder.suggest(mod.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.buildFuture();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Suggestions.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<String> getExamples() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +1,39 @@
|
||||||
# Do not change this
|
config-version = 1
|
||||||
config-version = "2.1"
|
# The player can only be synced to ONE forge server at a time.
|
||||||
|
# Resync will always happen if the player isn't synced to any server and is switching to a forge server.
|
||||||
|
[ReSync]
|
||||||
|
# In seconds. How much time the player has to reconnect before the resync cancels.
|
||||||
|
resync-timeout = 30
|
||||||
|
|
||||||
# How much time the player has to reconnect before canceling the server switch. (In seconds)
|
# Possible values: "Always", "Never".
|
||||||
# Only for players with ServerRedirect mod installed. Set to -1 to disable ServerRedirect mod support.
|
# Always: Always kicks the player while switching servers. The safest.
|
||||||
serverRedirectTimeout = 30
|
# Never: Switches server without kicking/resyncing the player. Can cause client crashes depending on the mods.
|
||||||
|
|
||||||
# Silence PCF absence warnings.
|
# When the player is connecting to a forge server while being synced to another.
|
||||||
silence-warnings = false
|
# Default: "always"
|
||||||
|
resync-forge-to-forge = "always"
|
||||||
|
# When the player is connecting to a vanilla server and is synced to a forge server.
|
||||||
|
# Default "never". "always" not recommended for servers with vanilla hubs/lobbies.
|
||||||
|
unsync-forge-to-vanilla = "never"
|
||||||
|
|
||||||
# Allow server switches without reset even though the new server's registries don't match the old server’s registry.
|
# Maybe you want to have one 1.16.5 modpack-server and one 1.18.2 modpack-server behind Velocity, in order for Ambassador to tell the
|
||||||
# Large modpacks often needs this set to true. Warning: This is a safety measure and setting this to true
|
# diffrence between modpacks on the connecting client, the plugin looks at the client's protocol version.
|
||||||
# can lead to unstable behaviour.
|
|
||||||
bypass-registry-checks = false
|
|
||||||
|
|
||||||
# Allow player to switch without reset when the server's mods don't match. Even more unstable than bypassRegistryCheck.
|
# You may add more diffrentiators, just make sure they have diffrent protocol versions.
|
||||||
# Warning: This is a safety measure for when bypassRegistryCheck is true. Setting this to also true can cause crashes.
|
[Differentiators]
|
||||||
bypass-mod-checks = false
|
|
||||||
|
|
||||||
# Only for debug/troubleshooting
|
# Protocol version - 1.18.2
|
||||||
debug-mode = false
|
[Differentiators.758]
|
||||||
|
# The server that players will initially sync to when connecting to the proxy.
|
||||||
|
# When left empty; Players will be able to join but will need to resync when joining connecting to a forge server.
|
||||||
|
# The name should be the same as specified in the "velocity.toml" config, e.g. "lobby".
|
||||||
|
default-forge-server = ""
|
||||||
|
|
||||||
# Make the player reconnect when server switching as a last resort if
|
# Some modpacks are not compatible with vanilla servers.
|
||||||
# the player can't reset or switch servers without resetting.
|
# To make sure they don't get connected to one upon login (like a vanilla lobby), change this to true.
|
||||||
enable-kick-reset = false
|
forced = false
|
||||||
|
|
||||||
# Message to display when player gets kicked due to server switching according to enable-kick-reset.
|
# Protocol version - 1.16.5
|
||||||
# This input is deserialzied as MiniMessage and support formating according to the MiniMessage format.
|
[Differentiators.754]
|
||||||
reconnect-message = "<red>Please reconnect.</red>"
|
default-forge-server = ""
|
||||||
|
forced = false
|
||||||
Loading…
Reference in New Issue
Block a user