Initial commit: LoParkour v1.1.0
This commit is contained in:
commit
aecb03ccf8
120 changed files with 9668 additions and 0 deletions
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Report a bug in LoParkour
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
Clear description of the bug.
|
||||
|
||||
## Steps to Reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected Behavior
|
||||
What should happen?
|
||||
|
||||
## Actual Behavior
|
||||
What actually happens?
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots.
|
||||
|
||||
## Console Errors
|
||||
```
|
||||
Paste console errors here
|
||||
```
|
||||
|
||||
## Environment
|
||||
- **LoParkour Version**:
|
||||
- **Server Software**: (Spigot/Paper/Purpur)
|
||||
- **Server Version**: (e.g. 1.20.4)
|
||||
- **Java Version**:
|
||||
- **Other Plugins**:
|
||||
|
||||
## Additional Context
|
||||
Any other relevant information.
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature for LoParkour
|
||||
title: '[FEATURE] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Feature Description
|
||||
Clear description of the feature you want.
|
||||
|
||||
## Use Case
|
||||
Why do you need this feature? What problem does it solve?
|
||||
|
||||
## Proposed Solution
|
||||
How should this feature work?
|
||||
|
||||
## Alternatives
|
||||
Have you considered any alternative solutions?
|
||||
|
||||
## Additional Context
|
||||
Any other relevant information, mockups, or examples.
|
||||
67
.gitignore
vendored
Normal file
67
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Compiled class files
|
||||
*.class
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# Virtual machine crash logs
|
||||
hs_err_pid*
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
.vscode/
|
||||
.settings/
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Gradle
|
||||
.gradle/
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!gradle/wrapper/gradle-wrapper.properties
|
||||
gradle-app.setting
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test files
|
||||
test-output/
|
||||
|
||||
# Modrinth files (local only)
|
||||
modrinth/
|
||||
|
||||
# TODO file (local only)
|
||||
TODO.md
|
||||
38
CHANGES.md
Normal file
38
CHANGES.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Changelog
|
||||
|
||||
## Version 6.0.0 (2026-02-24)
|
||||
|
||||
### 🔄 Major Changes
|
||||
- **Полная переработка проекта**
|
||||
- Название: LoParkour
|
||||
- Package: `dev.loki.loparkour`
|
||||
- Главный класс: `LoParkour`
|
||||
- Автор: loki
|
||||
|
||||
### 🏗️ Build System
|
||||
- **Миграция на Gradle**
|
||||
- Удалена Maven сборка (pom.xml)
|
||||
- Добавлен Gradle с Wrapper
|
||||
- Оптимизирована конфигурация зависимостей
|
||||
- Улучшена скорость сборки
|
||||
|
||||
### 📦 Technical
|
||||
- Обновлена версия PlaceholderAPI: 2.11.2 → 2.11.6
|
||||
- Исключена проблемная зависимость ConfigUpdater
|
||||
- Настроен Shadow plugin для shading
|
||||
- Добавлен .gitignore для Gradle
|
||||
|
||||
### 🎯 Commands
|
||||
- Новая команда: `/loparkour` (aliases: `/parkour`, `/lp`)
|
||||
- Старая команда `/witp` удалена
|
||||
|
||||
### 📝 Documentation
|
||||
- Создан TODO.md с детальным планом развития
|
||||
- Обновлен README.md
|
||||
- Добавлены инструкции по сборке
|
||||
|
||||
---
|
||||
|
||||
## Version 1.0.0 and earlier
|
||||
|
||||
Initial development versions.
|
||||
675
LICENSE
Normal file
675
LICENSE
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
{project} Copyright (C) {year} {fullname}
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
30
README.md
Normal file
30
README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<div align="center">
|
||||
<h3>LoParkour</h3>
|
||||
<strong>
|
||||
Advanced infinitely generating parkour plugin for Minecraft
|
||||
<br>
|
||||
by <a href="https://github.com/loki">loki</a>
|
||||
</strong>
|
||||
<br><br>
|
||||
</div>
|
||||
|
||||
## 🚀 Building
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
gradlew.bat clean build
|
||||
|
||||
# Linux/Mac
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
Output: `build/libs/LoParkour-6.0.0.jar`
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- Java 17+
|
||||
- Spigot/Paper 1.20.4+
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
See [TODO.md](TODO.md) for planned features and improvements.
|
||||
134
build.gradle
Normal file
134
build.gradle
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
}
|
||||
|
||||
group = 'dev.loki'
|
||||
version = '1.1.0'
|
||||
description = 'LoParkour - Advanced parkour plugin for Minecraft'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
maven {
|
||||
name = 'papermc'
|
||||
url = 'https://repo.papermc.io/repository/maven-public/'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'spigot-repo'
|
||||
url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'jitpack'
|
||||
url = 'https://jitpack.io'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'maven-central'
|
||||
url = 'https://oss.sonatype.org/content/groups/public'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'placeholderapi'
|
||||
url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'mv-repo'
|
||||
url = 'https://repo.onarandombox.com/content/groups/public/'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'dmulloy2-repo'
|
||||
url = 'https://repo.dmulloy2.net/repository/public/'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'codemc-repo'
|
||||
url = 'https://repo.codemc.io/repository/maven-public/'
|
||||
}
|
||||
|
||||
maven {
|
||||
name = 'opencollab-snapshot-repo'
|
||||
url = 'https://repo.opencollab.dev/maven-snapshots/'
|
||||
}
|
||||
|
||||
// For ConfigUpdater dependency
|
||||
maven {
|
||||
name = 'codemc-snapshots'
|
||||
url = 'https://repo.codemc.io/repository/maven-snapshots/'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Spigot API
|
||||
compileOnly 'org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT'
|
||||
|
||||
// Shaded dependencies
|
||||
implementation('com.github.Efnilite:vilib:dd786231c3') {
|
||||
exclude group: 'com.tchristofferson', module: 'ConfigUpdater'
|
||||
}
|
||||
implementation 'io.papermc:paperlib:1.0.7'
|
||||
|
||||
// Provided dependencies (plugins)
|
||||
compileOnly 'me.clip:placeholderapi:2.11.6'
|
||||
compileOnly 'com.github.MilkBowl:VaultAPI:1.7'
|
||||
compileOnly 'me.filoghost.holographicdisplays:holographicdisplays-api:3.0.0-SNAPSHOT'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.0-SNAPSHOT'
|
||||
|
||||
// Annotations
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
|
||||
// Testing
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0-M1'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
archiveClassifier.set('')
|
||||
archiveFileName.set("LoParkour-${project.version}.jar")
|
||||
|
||||
// Relocate shaded dependencies
|
||||
relocate 'dev.efnilite.vilib', 'dev.loki.loparkour.lib.vilib'
|
||||
relocate 'io.papermc.lib', 'dev.loki.loparkour.lib.paperlib'
|
||||
|
||||
// Minimize jar size
|
||||
minimize()
|
||||
}
|
||||
|
||||
build {
|
||||
dependsOn shadowJar
|
||||
}
|
||||
|
||||
processResources {
|
||||
filesMatching(['plugin.yml', 'config.yml']) {
|
||||
expand(
|
||||
version: project.version,
|
||||
description: project.description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.release = 17
|
||||
}
|
||||
|
||||
javadoc {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
// Default task
|
||||
defaultTasks 'clean', 'build'
|
||||
8
gradle.properties
Normal file
8
gradle.properties
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Gradle properties
|
||||
org.gradle.jvmargs=-Xmx2G
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.daemon=true
|
||||
|
||||
# Project properties
|
||||
kotlin.code.style=official
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
248
gradlew
vendored
Normal file
248
gradlew
vendored
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
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:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
93
gradlew.bat
vendored
Normal file
93
gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
settings.gradle
Normal file
1
settings.gradle
Normal file
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = 'LoParkour'
|
||||
546
src/main/java/dev/loki/loparkour/Command.java
Normal file
546
src/main/java/dev/loki/loparkour/Command.java
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
package dev.loki.loparkour;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.mode.MultiMode;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.player.data.InventoryData;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.command.ViCommand;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.particle.ParticleData;
|
||||
import dev.efnilite.vilib.particle.Particles;
|
||||
import dev.efnilite.vilib.schematic.Schematic;
|
||||
import dev.efnilite.vilib.schematic.Schematics;
|
||||
import dev.efnilite.vilib.util.Locations;
|
||||
import dev.efnilite.vilib.util.Strings;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.command.BlockCommandSender;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class Command extends ViCommand {
|
||||
|
||||
public static final HashMap<Player, Location[]> selections = new HashMap<>();
|
||||
|
||||
private static final ItemStack WAND = new Item(Material.GOLDEN_AXE, "<red><bold>Schematic Wand")
|
||||
.lore("<gray>Left click: first position", "<gray>Right click: second position")
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public boolean execute(CommandSender sender, String[] args) {
|
||||
Player player = null;
|
||||
if (sender instanceof Player) {
|
||||
player = (Player) sender;
|
||||
}
|
||||
|
||||
switch (args.length) {
|
||||
case 0 -> handle0Args(sender, player);
|
||||
case 1 -> handle1Args(args[0], sender, player);
|
||||
case 2 -> handle2Args(args[0], args[1], sender, player);
|
||||
case 3 -> handle3Args(args[0], args[1], args[2], sender, player);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> tabComplete(CommandSender sender, String[] args) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
switch (args.length) {
|
||||
case 1 -> {
|
||||
if (ParkourOption.JOIN.mayPerform(sender)) {
|
||||
completions.add("join");
|
||||
completions.add("leave");
|
||||
}
|
||||
if (ParkourOption.MAIN.mayPerform(sender)) {
|
||||
completions.add("menu");
|
||||
}
|
||||
if (ParkourOption.PLAY.mayPerform(sender)) {
|
||||
completions.add("play");
|
||||
}
|
||||
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||
completions.add("leaderboard");
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("schematic");
|
||||
completions.add("reload");
|
||||
completions.add("forcejoin");
|
||||
completions.add("forceleave");
|
||||
completions.add("reset");
|
||||
completions.add("recoverinventory");
|
||||
}
|
||||
return completions(args[0], completions);
|
||||
}
|
||||
case 2 -> {
|
||||
if (args[0].equalsIgnoreCase("reset") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("everyone");
|
||||
for (ParkourPlayer pp : ParkourPlayer.getPlayers()) {
|
||||
completions.add(pp.getName());
|
||||
}
|
||||
} else if (args[0].equalsIgnoreCase("join") && sender.hasPermission(ParkourOption.JOIN.permission)) {
|
||||
for (ParkourPlayer pp : ParkourPlayer.getPlayers()) {
|
||||
completions.add(pp.getName());
|
||||
}
|
||||
} else if (args[0].equalsIgnoreCase("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste"));
|
||||
} else if (args[0].equalsIgnoreCase("forcejoin") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("nearest");
|
||||
completions.add("everyone");
|
||||
|
||||
for (Player pl : Bukkit.getOnlinePlayers()) {
|
||||
completions.add(pl.getName());
|
||||
}
|
||||
} else if (args[0].equalsIgnoreCase("forceleave") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("everyone");
|
||||
|
||||
for (Player pl : Bukkit.getOnlinePlayers()) {
|
||||
completions.add(pl.getName());
|
||||
}
|
||||
} else if (args[0].equalsIgnoreCase("recoverinventory") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
for (Player pl : Bukkit.getOnlinePlayers()) {
|
||||
completions.add(pl.getName());
|
||||
}
|
||||
}
|
||||
return completions(args[1], completions);
|
||||
}
|
||||
default -> {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handle0Args(@NotNull CommandSender sender, @Nullable Player player) {
|
||||
if (player != null && ParkourOption.MAIN.mayPerform(player)) {
|
||||
Menus.MAIN.open(player);
|
||||
return;
|
||||
}
|
||||
sendHelpMessages(sender);
|
||||
}
|
||||
|
||||
private void sendHelpMessages(CommandSender sender) {
|
||||
send(sender, "");
|
||||
send(sender, "<dark_gray><strikethrough>---------------<reset> %s <dark_gray><strikethrough>---------------<reset>".formatted(LoParkour.NAME));
|
||||
send(sender, "");
|
||||
send(sender, "<gray>/parkour <dark_gray>- Main command");
|
||||
if (sender.hasPermission(ParkourOption.JOIN.permission)) {
|
||||
send(sender, "<gray>/parkour join [mode/player] <dark_gray>- Join the default mode or specify one.");
|
||||
send(sender, "<gray>/parkour leave <dark_gray>- Leave the game on this server");
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.MAIN.permission)) {
|
||||
send(sender, "<gray>/parkour menu <dark_gray>- Open the menu");
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.PLAY.permission)) {
|
||||
send(sender, "<gray>/parkour play <dark_gray>- Mode selection menu");
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) {
|
||||
send(sender, "<gray>/parkour leaderboard [type]<dark_gray>- Open the leaderboard of a mode");
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
send(sender, "<gray>/LoParkour schematic <dark_gray>- Create a schematic");
|
||||
send(sender, "<gray>/LoParkour reload <dark_gray>- Reloads the messages-v3.yml file");
|
||||
send(sender, "<gray>/LoParkour reset <everyone/player> <dark_gray>- Resets all high scores. <red>This can't be recovered!");
|
||||
send(sender, "<gray>/LoParkour forcejoin <everyone/nearest/player> <dark_gray>- Forces a specific player, the nearest or everyone to join");
|
||||
send(sender, "<gray>/LoParkour forceleave <everyone/nearest/player> <dark_gray>- Forces a specific player, the nearest or everyone to leave");
|
||||
send(sender, "<gray>/LoParkour recoverinventory <player> <dark_gray>- Recover a player's saved inventory. <red>Useful for recovering data after server crashes or errors when leaving.");
|
||||
}
|
||||
send(sender, "");
|
||||
}
|
||||
|
||||
private void handle1Args(@NotNull String arg, @NotNull CommandSender sender, @Nullable Player player) {
|
||||
switch (arg.toLowerCase()) {
|
||||
case "help" -> sendHelpMessages(sender);
|
||||
case "reload" -> {
|
||||
if (!cooldown(sender, "reload", 2500)) {
|
||||
return;
|
||||
}
|
||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
send(sender, Locales.getString(player, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
|
||||
Config.reload(false);
|
||||
|
||||
send(sender, "%sReloaded config files.".formatted(LoParkour.PREFIX));
|
||||
}
|
||||
}
|
||||
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (arg.toLowerCase()) {
|
||||
case "join" -> {
|
||||
if (!cooldown(sender, "join", 2500)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourOption.JOIN.mayPerform(player)) {
|
||||
send(sender, Locales.getString(player, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
if (user != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Modes.DEFAULT.create(player);
|
||||
}
|
||||
case "play" -> {
|
||||
if (ParkourOption.PLAY.mayPerform(player)) {
|
||||
Menus.PLAY.open(player);
|
||||
}
|
||||
}
|
||||
case "leave" -> {
|
||||
if (!cooldown(sender, "leave", 2500)) {
|
||||
return;
|
||||
}
|
||||
ParkourUser.leave(player);
|
||||
}
|
||||
case "menu", "main" -> {
|
||||
if (!ParkourOption.MAIN.mayPerform(player)) {
|
||||
Menus.MAIN.open(player);
|
||||
}
|
||||
}
|
||||
case "leaderboard" -> player.performCommand("LoParkour leaderboard invalid");
|
||||
case "schematic" -> {
|
||||
if (!player.hasPermission(ParkourOption.ADMIN.permission)) { // default players shouldn't have access even if perms are disabled
|
||||
send(sender, Locales.getString(player, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
|
||||
send(player, "");
|
||||
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the schematic wand");
|
||||
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set the first position of your selection");
|
||||
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set the second position of your selection");
|
||||
send(player, "<red>/LoParkour schematic save <dark_gray>- <gray>Save your selection to a schematic file");
|
||||
send(player, "<red>/LoParkour schematic paste <file> <dark_gray>- <gray>Paste a schematic file");
|
||||
send(player, "");
|
||||
send(player, "<dark_gray><underlined>Have any questions or need help? Join the Discord.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handle2Args(@NotNull String arg1, @NotNull String arg2, @NotNull CommandSender sender, @Nullable Player player) {
|
||||
switch (arg1.toLowerCase()) {
|
||||
case "forcejoin" -> {
|
||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg2.equalsIgnoreCase("everyone")) {
|
||||
Bukkit.getOnlinePlayers().forEach(other -> Modes.DEFAULT.create(other));
|
||||
send(sender, LoParkour.PREFIX + "Successfully force joined everyone!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg2.equalsIgnoreCase("nearest")) {
|
||||
Player closest = null;
|
||||
double distance = Double.MAX_VALUE;
|
||||
|
||||
// if player is found get location from player
|
||||
// if no player is found, get location from command block
|
||||
// if no command block is found, return null
|
||||
Location from = sender instanceof Player ? ((Player) sender).getLocation() : (sender instanceof BlockCommandSender ? ((BlockCommandSender) sender).getBlock().getLocation() : null);
|
||||
|
||||
if (from == null || from.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get the closest player
|
||||
for (Player p : from.getWorld().getPlayers()) {
|
||||
double d = p.getLocation().distance(from);
|
||||
|
||||
if (d < distance) {
|
||||
distance = d;
|
||||
closest = p;
|
||||
}
|
||||
}
|
||||
|
||||
// no closest player found
|
||||
if (closest == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
send(sender, LoParkour.PREFIX + "Successfully force joined " + closest.getName() + "!");
|
||||
Modes.DEFAULT.create(closest);
|
||||
return;
|
||||
}
|
||||
|
||||
Player other = Bukkit.getPlayer(arg2);
|
||||
if (other == null) {
|
||||
send(sender, LoParkour.PREFIX + "That player isn't online!");
|
||||
return;
|
||||
}
|
||||
|
||||
Modes.DEFAULT.create(other);
|
||||
}
|
||||
case "forceleave" -> {
|
||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg2.equalsIgnoreCase("everyone")) {
|
||||
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
|
||||
send(sender, LoParkour.PREFIX + "Successfully force kicked everyone!");
|
||||
return;
|
||||
}
|
||||
|
||||
Player other = Bukkit.getPlayer(arg2);
|
||||
if (other == null) {
|
||||
send(sender, LoParkour.PREFIX + "That player isn't online!");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(other);
|
||||
if (user == null) {
|
||||
send(sender, LoParkour.PREFIX + "That player isn't currently playing!");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser.leave(user);
|
||||
}
|
||||
case "reset" -> {
|
||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission) || !cooldown(sender, "reset", 2500)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg2.equalsIgnoreCase("everyone")) {
|
||||
for (Mode mode : Registry.getModes()) {
|
||||
Leaderboard leaderboard = mode.getLeaderboard();
|
||||
|
||||
if (leaderboard == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
leaderboard.resetAll();
|
||||
leaderboard.write(true);
|
||||
}
|
||||
|
||||
send(sender, LoParkour.PREFIX + "Successfully reset all high scores in memory and the files.");
|
||||
return;
|
||||
}
|
||||
String name = null;
|
||||
UUID uuid = null;
|
||||
|
||||
// Check online players
|
||||
Player online = Bukkit.getPlayerExact(arg2);
|
||||
if (online != null) {
|
||||
name = online.getName();
|
||||
uuid = online.getUniqueId();
|
||||
}
|
||||
|
||||
// Check uuid
|
||||
if (arg2.contains("-")) {
|
||||
uuid = UUID.fromString(arg2);
|
||||
}
|
||||
|
||||
// Check offline player
|
||||
if (uuid == null) {
|
||||
OfflinePlayer offline = Bukkit.getOfflinePlayer(arg2);
|
||||
name = offline.getName();
|
||||
uuid = offline.getUniqueId();
|
||||
}
|
||||
|
||||
UUID finalUuid = uuid;
|
||||
String finalName = name;
|
||||
|
||||
for (Mode mode : Registry.getModes()) {
|
||||
Leaderboard leaderboard = mode.getLeaderboard();
|
||||
|
||||
if (leaderboard == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
leaderboard.remove(finalUuid);
|
||||
leaderboard.write(true);
|
||||
}
|
||||
|
||||
send(sender, LoParkour.PREFIX + "Successfully reset the high score of " + finalName + " in memory and the files.");
|
||||
}
|
||||
case "recoverinventory" -> {
|
||||
if (!cooldown(sender, "recoverinventory", 2500) || !sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player other = Bukkit.getPlayer(arg2);
|
||||
if (other == null) {
|
||||
send(sender, LoParkour.PREFIX + "That player isn't online!");
|
||||
return;
|
||||
}
|
||||
|
||||
new InventoryData(other).load(result -> {
|
||||
if (result != null) {
|
||||
send(sender, "%sSuccessfully recovered the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName()));
|
||||
send(sender, "%sGiving %s their items now...".formatted(LoParkour.PREFIX, other.getName()));
|
||||
} else {
|
||||
send(sender, "%s<red>There was an error recovering the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName()));
|
||||
send(sender, "%s%s has no saved inventory or there was an error. Check the console.".formatted(LoParkour.PREFIX, other.getName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (arg1) {
|
||||
case "join" -> {
|
||||
if (!cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mode mode = Registry.getMode(arg2);
|
||||
|
||||
if (mode != null) {
|
||||
mode.create(player);
|
||||
return;
|
||||
}
|
||||
|
||||
Player other = Bukkit.getPlayer(arg2);
|
||||
|
||||
if (other == null) {
|
||||
send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer parkourPlayer = ParkourPlayer.getPlayer(other);
|
||||
|
||||
if (parkourPlayer == null) {
|
||||
send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
Session session = parkourPlayer.session;
|
||||
if (user != null && user.session == session) { // already in same session
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAcceptingPlayers()) {
|
||||
((MultiMode) session.generator.getMode()).join(player, session);
|
||||
} else {
|
||||
Modes.SPECTATOR.create(player, session);
|
||||
}
|
||||
}
|
||||
case "leaderboard" -> {
|
||||
if (!ParkourOption.LEADERBOARDS.mayPerform(player)) {
|
||||
send(sender, Locales.getString(player, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
|
||||
Mode mode = Registry.getMode(arg2.toLowerCase());
|
||||
|
||||
// if found gamemode is null, return to default
|
||||
if (mode == null) {
|
||||
Menus.LEADERBOARDS.open(player);
|
||||
} else {
|
||||
Menus.SINGLE_LEADERBOARD.open(player, mode, Leaderboard.Sort.SCORE);
|
||||
}
|
||||
}
|
||||
case "schematic" -> {
|
||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
send(sender, Locales.getString(player, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
|
||||
Location playerLocation = player.getLocation();
|
||||
Location[] existingSelection = selections.get(player);
|
||||
|
||||
switch (arg2.toLowerCase()) {
|
||||
case "wand" -> {
|
||||
player.getInventory().addItem(WAND);
|
||||
|
||||
send(player, "<dark_gray>----------- <dark_red><bold>Schematics <reset><dark_gray>-----------");
|
||||
send(player, "<gray><red>Left click<gray> -> set first position | <red>Right click<gray> -> set second position");
|
||||
send(player, "<gray>If you can't place a block and need to set a position mid-air, use <dark_gray>/LoParkour schematic pos1/pos2 <gray>instead.");
|
||||
}
|
||||
case "pos1" -> {
|
||||
send(player, "%sPosition 1 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true)));
|
||||
|
||||
if (existingSelection == null) {
|
||||
selections.put(player, new Location[]{playerLocation, null});
|
||||
return;
|
||||
}
|
||||
|
||||
selections.put(player, new Location[]{playerLocation, existingSelection[1]});
|
||||
|
||||
Particles.box(BoundingBox.of(playerLocation, existingSelection[1]), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), player, 0.2);
|
||||
}
|
||||
case "pos2" -> {
|
||||
send(player, "%sPosition 2 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true)));
|
||||
|
||||
if (existingSelection == null) {
|
||||
selections.put(player, new Location[]{null, playerLocation});
|
||||
return;
|
||||
}
|
||||
|
||||
selections.put(player, new Location[]{existingSelection[0], playerLocation});
|
||||
|
||||
Particles.box(BoundingBox.of(existingSelection[0], playerLocation), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), player, 0.2);
|
||||
}
|
||||
case "save" -> {
|
||||
if (!cooldown(sender, "LoParkour save schematic", 2500)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingSelection == null || existingSelection[0] == null || existingSelection[1] == null) {
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Your schematic isn't complete yet. Make sure you've set the first and second position.");
|
||||
return;
|
||||
}
|
||||
|
||||
String code = UUID.randomUUID().toString().split("-")[0];
|
||||
|
||||
send(player, ("<dark_red><bold>Schematics <reset><gray>Your schematic is being saved. It will use code <red>'%s'<gray>. " + "You can change the code to whatever you like. " + "Don't forget to add this schematic to <dark_gray>schematics.yml<gray>.").formatted(code));
|
||||
|
||||
Schematic.save(LoParkour.getInFolder("schematics/parkour-%s".formatted(code)), existingSelection[0], existingSelection[1], LoParkour.getPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handle3Args(@NotNull String arg1, @NotNull String arg2, @NotNull String arg3, @NotNull CommandSender sender, @Nullable Player player) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg1.equalsIgnoreCase("schematic")) {
|
||||
if (!arg2.equalsIgnoreCase("paste")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schematic schematic = Schematics.getSchematic(LoParkour.getPlugin(), arg3);
|
||||
if (schematic == null) {
|
||||
send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3));
|
||||
return;
|
||||
}
|
||||
|
||||
schematic.paste(player.getLocation());
|
||||
send(sender, "%sPasted schematic %s".formatted(LoParkour.PREFIX, arg3));
|
||||
}
|
||||
}
|
||||
|
||||
private void send(CommandSender sender, String message) {
|
||||
sender.sendMessage(Strings.colour(message));
|
||||
}
|
||||
}
|
||||
316
src/main/java/dev/loki/loparkour/Events.java
Normal file
316
src/main/java/dev/loki/loparkour/Events.java
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
package dev.loki.loparkour;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.world.World;
|
||||
import dev.efnilite.vilib.event.EventWatcher;
|
||||
import dev.efnilite.vilib.particle.ParticleData;
|
||||
import dev.efnilite.vilib.particle.Particles;
|
||||
import dev.efnilite.vilib.util.Locations;
|
||||
import dev.efnilite.vilib.util.Strings;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.event.player.*;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Internal event handler
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public class Events implements EventWatcher {
|
||||
|
||||
@EventHandler
|
||||
public void chat(AsyncPlayerChatEvent event) {
|
||||
if (!Option.OPTIONS_ENABLED.get(ParkourOption.CHAT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Session session = user.session;
|
||||
|
||||
if (session.isMuted(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
switch (user.chatType) {
|
||||
case LOBBY_ONLY -> session.getUsers().forEach(other -> other.sendTranslated("settings.chat.formats.lobby", player.getName(), event.getMessage()));
|
||||
case PLAYERS_ONLY -> session.getPlayers().forEach(other -> other.sendTranslated("settings.chat.formats.players", player.getName(), event.getMessage()));
|
||||
default -> event.setCancelled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void join(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
||||
Modes.DEFAULT.create(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.getWorld().equals(World.getWorld())) {
|
||||
return;
|
||||
}
|
||||
|
||||
org.bukkit.World fallback = Bukkit.getWorld(Config.CONFIG.getString("world.fall-back"));
|
||||
|
||||
if (fallback != null) {
|
||||
PaperLib.teleportAsync(player, fallback.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
PaperLib.teleportAsync(player, Bukkit.getWorlds().stream()
|
||||
.filter(world -> !world.equals(World.getWorld()))
|
||||
.findAny()
|
||||
.orElseThrow(() -> new NoSuchElementException("No fallback world was found!"))
|
||||
.getSpawnLocation());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void leave(PlayerQuitEvent event) {
|
||||
ParkourUser user = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser.unregister(user, true, false, true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void command(PlayerCommandPreprocessEvent event) {
|
||||
if (!Config.CONFIG.getBoolean("focus-mode.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String command = event.getMessage().toLowerCase();
|
||||
if (Config.CONFIG.getStringList("focus-mode.whitelist").stream().anyMatch(c -> command.contains(c.toLowerCase()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
user.sendTranslated("other.no_do");
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
|
||||
@EventHandler
|
||||
public void interactWand(PlayerInteractEvent event) {
|
||||
Action action = event.getAction();
|
||||
Player player = event.getPlayer();
|
||||
ItemStack item = player.getInventory().getItemInMainHand();
|
||||
|
||||
if (!player.hasPermission("LoParkour.admin") || item.getItemMeta() == null || !item.getItemMeta().getDisplayName().contains("Schematic Wand") || event.getClickedBlock() == null || event.getHand() != EquipmentSlot.HAND) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = event.getClickedBlock().getLocation();
|
||||
Location[] existingSelection = Command.selections.get(player);
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
switch (action) {
|
||||
case LEFT_CLICK_BLOCK -> {
|
||||
send(player, LoParkour.PREFIX + "Position 1 was set to " + Locations.toString(location, true));
|
||||
|
||||
if (existingSelection == null) {
|
||||
Command.selections.put(player, new Location[]{location, null});
|
||||
return;
|
||||
}
|
||||
|
||||
Command.selections.put(player, new Location[]{location, existingSelection[1]});
|
||||
|
||||
Particles.box(BoundingBox.of(location, existingSelection[1]), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), player, 0.2);
|
||||
}
|
||||
case RIGHT_CLICK_BLOCK -> {
|
||||
send(player, LoParkour.PREFIX + "Position 2 was set to " + Locations.toString(location, true));
|
||||
|
||||
if (existingSelection == null) {
|
||||
Command.selections.put(player, new Location[]{null, location});
|
||||
return;
|
||||
}
|
||||
|
||||
Command.selections.put(player, new Location[]{existingSelection[0], location});
|
||||
|
||||
Particles.box(BoundingBox.of(existingSelection[0], location), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), player, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void interact(PlayerInteractEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
|
||||
if (pp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean type = event.getClickedBlock() != null && (event.getClickedBlock().getType() == Material.DISPENSER || event.getClickedBlock().getType() == Material.DROPPER || event.getClickedBlock().getType() == Material.HOPPER);
|
||||
|
||||
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && type && event.getHand() == EquipmentSlot.HAND) {
|
||||
event.setCancelled(true);
|
||||
player.closeInventory();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean action = (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) && event.getHand() == EquipmentSlot.HAND;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
Material held = getHeldItem(player).getType();
|
||||
|
||||
Material play = Locales.getItem(player, "play.item").getMaterial();
|
||||
Material community = Locales.getItem(player, "community.item").getMaterial();
|
||||
Material settings = Locales.getItem(player, "settings.item").getMaterial();
|
||||
Material lobby = Locales.getItem(player, "lobby.item").getMaterial();
|
||||
Material quit = Locales.getItem(player, "other.quit").getMaterial();
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
if (held == play) {
|
||||
Menus.PLAY.open(player);
|
||||
} else if (held == community) {
|
||||
Menus.COMMUNITY.open(player);
|
||||
} else if (held == settings) {
|
||||
Menus.SETTINGS.open(player);
|
||||
} else if (held == lobby) {
|
||||
Menus.LOBBY.open(player);
|
||||
} else if (held == quit) {
|
||||
ParkourUser.leave(player);
|
||||
} else {
|
||||
if (!Config.CONFIG.getBoolean("options.disable-inventory-blocks")) {
|
||||
event.setCancelled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack getHeldItem(Player player) {
|
||||
PlayerInventory inventory = player.getInventory();
|
||||
return inventory.getItemInMainHand().getType() == Material.AIR ? inventory.getItemInOffHand() : inventory.getItemInMainHand();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void switchWorld(PlayerChangedWorldEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
org.bukkit.World parkour = World.getWorld();
|
||||
|
||||
boolean isAdmin = Config.CONFIG.getBoolean("permissions.enabled") ? ParkourOption.ADMIN.mayPerform(player) : player.isOp();
|
||||
|
||||
if (player.getWorld() == parkour && user == null && !isAdmin && player.getTicksLived() > 20) {
|
||||
Bukkit.getWorlds().stream()
|
||||
.filter(world -> !world.equals(parkour))
|
||||
.findAny()
|
||||
.ifPresent(world -> PaperLib.teleportAsync(player, world.getSpawnLocation()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getFrom() == parkour && user != null && Duration.between(user.joined, Instant.now()).toMillis() > 100) {
|
||||
ParkourUser.unregister(user, true, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDrop(PlayerDropItemEvent event) {
|
||||
handleRestriction(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlace(BlockPlaceEvent event) {
|
||||
handleRestriction(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBreak(BlockBreakEvent event) {
|
||||
handleRestriction(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void damage(EntityDamageEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleRestriction(player, event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void inventory(InventoryClickEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player player) || event.getInventory().getType() == InventoryType.CRAFTING) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleRestriction(player, event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void spectate(PlayerTeleportEvent event) {
|
||||
if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleRestriction(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
// @EventHandler(priority = EventPriority.HIGHEST)
|
||||
// public void mount(EntityMountEvent event) {
|
||||
// if (!(event.getEntity() instanceof Player player)) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// handleRestriction(player, event);
|
||||
// }
|
||||
|
||||
private void handleRestriction(Player player, Cancellable event) {
|
||||
if (!ParkourUser.isUser(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
private void send(CommandSender sender, String message) {
|
||||
sender.sendMessage(Strings.colour(message));
|
||||
}
|
||||
}
|
||||
156
src/main/java/dev/loki/loparkour/LoParkour.java
Normal file
156
src/main/java/dev/loki/loparkour/LoParkour.java
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
package dev.loki.loparkour;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.hook.HoloHook;
|
||||
import dev.loki.loparkour.hook.PAPIHook;
|
||||
import dev.loki.loparkour.mode.DefaultMode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.mode.SpectatorMode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.World;
|
||||
import dev.efnilite.vilib.ViPlugin;
|
||||
import dev.efnilite.vilib.bstats.bukkit.Metrics;
|
||||
import dev.efnilite.vilib.bstats.charts.SimplePie;
|
||||
import dev.efnilite.vilib.bstats.charts.SingleLineChart;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.util.Logging;
|
||||
import dev.efnilite.vilib.util.UpdateChecker;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Main class of LoParkour
|
||||
*
|
||||
* @author loki
|
||||
*/
|
||||
public final class LoParkour extends ViPlugin {
|
||||
|
||||
public static final String NAME = "&#FF6464<bold>LoParkour<reset>";
|
||||
public static final String PREFIX = NAME + " 񢩈» &#A0A0A0";
|
||||
|
||||
private static Logging logging;
|
||||
private static LoParkour instance;
|
||||
|
||||
@Nullable
|
||||
private static PAPIHook placeholderHook;
|
||||
|
||||
public static void log(String message) {
|
||||
if (Config.CONFIG.getBoolean("debug")) {
|
||||
logging.info("[Debug] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param child The file name.
|
||||
* @return A file from within the plugin folder.
|
||||
*/
|
||||
public static File getInFolder(String child) {
|
||||
return new File(instance.getDataFolder(), child);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return This plugin's {@link Logging} instance.
|
||||
*/
|
||||
public static Logging logging() {
|
||||
return logging;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The plugin instance.
|
||||
*/
|
||||
public static LoParkour getPlugin() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PAPIHook getPlaceholderHook() {
|
||||
return placeholderHook;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
logging = new Logging(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
|
||||
// ----- Configurations -----
|
||||
|
||||
Config.reload(true);
|
||||
|
||||
// ----- Registry -----
|
||||
|
||||
Registry.register(new DefaultMode());
|
||||
Registry.register(new SpectatorMode());
|
||||
|
||||
Modes.init();
|
||||
Menu.init(this);
|
||||
|
||||
// hook with hd / papi after gamemode leaderboards have initialized
|
||||
if (getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) {
|
||||
logging.info("Registered Holographic Displays hook");
|
||||
HoloHook.init();
|
||||
}
|
||||
|
||||
if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||
logging.info("Registered PlaceholderAPI hook");
|
||||
placeholderHook = new PAPIHook();
|
||||
placeholderHook.register();
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
|
||||
logging.info("Registered BungeeCord hook");
|
||||
}
|
||||
|
||||
// ----- Worlds -----
|
||||
|
||||
if (Config.CONFIG.getBoolean("joining")) {
|
||||
World.create();
|
||||
}
|
||||
|
||||
// ----- Events -----
|
||||
|
||||
registerListener(new Events());
|
||||
registerCommand("LoParkour", new Command());
|
||||
|
||||
// ----- Metrics -----
|
||||
|
||||
Metrics metrics = new Metrics(this, 9272);
|
||||
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL)));
|
||||
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
||||
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.locales.size())));
|
||||
metrics.addCustomChart(new SingleLineChart("player_joins", () -> {
|
||||
int joins = ParkourUser.joinCount;
|
||||
ParkourUser.joinCount = 0;
|
||||
return joins;
|
||||
}));
|
||||
|
||||
UpdateChecker.check(this, 87226);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
try {
|
||||
for (ParkourUser user : ParkourUser.getUsers()) {
|
||||
ParkourUser.leave(user);
|
||||
}
|
||||
|
||||
// write all LoParkour gamemodes
|
||||
Modes.DEFAULT.getLeaderboard().write(false);
|
||||
|
||||
Storage.close();
|
||||
World.delete();
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/main/java/dev/loki/loparkour/api/ParkourAPI.java
Normal file
42
src/main/java/dev/loki/loparkour/api/ParkourAPI.java
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.loparkour.api;
|
||||
|
||||
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Main API handler.
|
||||
* <ul>
|
||||
* <li>For general player handling, please view {@link ParkourUser}.</li>
|
||||
* <li>For parkour players, please view {@link ParkourPlayer}.</li>
|
||||
* <li>For player spectating, please view {@link dev.efnilite.LoParkour.player.ParkourSpectator}.</li>
|
||||
* <li>For events, please view {@link ParkourBlockGenerateEvent} and others in the events package.</li>
|
||||
* <li>For Sessions, please view {@link dev.efnilite.LoParkour.session.Session}.</li>
|
||||
* <li>For Schematics, please view {@link dev.efnilite.LoParkour.schematic.Schematic}</li>
|
||||
* </ul>
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ParkourAPI {
|
||||
|
||||
private ParkourAPI() throws IllegalAccessException {
|
||||
throw new IllegalAccessException("Initializing API class");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The player.
|
||||
* @return The {@link ParkourPlayer}. Null if not found.
|
||||
*/
|
||||
public static @Nullable ParkourPlayer getPlayer(Player player) {
|
||||
return ParkourPlayer.getPlayer(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The player.
|
||||
* @return The {@link ParkourPlayer}. Null if not found.
|
||||
*/
|
||||
public static @Nullable ParkourUser getUser(Player player) {
|
||||
return ParkourUser.getUser(player);
|
||||
}
|
||||
}
|
||||
67
src/main/java/dev/loki/loparkour/api/Registry.java
Normal file
67
src/main/java/dev/loki/loparkour/api/Registry.java
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package dev.loki.loparkour.api;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Registers stuff.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public final class Registry {
|
||||
|
||||
private static final LinkedList<Mode> modes = new LinkedList<>();
|
||||
private static final LinkedList<Style> styles = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Registers a {@link Mode}.
|
||||
*
|
||||
* @param mode The mode.
|
||||
*/
|
||||
public static void register(@NotNull Mode mode) {
|
||||
modes.add(mode);
|
||||
|
||||
LoParkour.log("Registered mode %s".formatted(mode.getName()));
|
||||
}
|
||||
|
||||
public static void register(@NotNull Style style) {
|
||||
styles.add(style);
|
||||
|
||||
LoParkour.log("Registered style %s".formatted(style.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The mode name.
|
||||
* @return The {@link Mode} instance. May be null.
|
||||
*/
|
||||
@Nullable
|
||||
public static Mode getMode(@NotNull String name) {
|
||||
return modes.stream()
|
||||
.filter(mode -> mode.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Style getStyle(@NotNull String name) {
|
||||
return styles.stream()
|
||||
.filter(style -> style.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static List<Style> getStyles() {
|
||||
return styles;
|
||||
}
|
||||
|
||||
public static List<Mode> getModes() {
|
||||
return modes;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.efnilite.vilib.event.EventWrapper;
|
||||
import org.bukkit.block.Block;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Gets called when a new jump is generated. Read-only.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class ParkourBlockGenerateEvent extends EventWrapper {
|
||||
|
||||
public final List<Block> blocks;
|
||||
public final ParkourGenerator generator;
|
||||
public final ParkourPlayer player;
|
||||
|
||||
public ParkourBlockGenerateEvent(List<Block> blocks, ParkourGenerator generator, ParkourPlayer player) {
|
||||
this.blocks = blocks;
|
||||
this.generator = generator;
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.efnilite.vilib.event.EventWrapper;
|
||||
|
||||
/**
|
||||
* Gets called when a player falls. Read-only.
|
||||
*/
|
||||
public class ParkourFallEvent extends EventWrapper {
|
||||
|
||||
public final ParkourPlayer player;
|
||||
|
||||
public ParkourFallEvent(ParkourPlayer player) {
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.event.EventWrapper;
|
||||
|
||||
/**
|
||||
* Gets called when a player joins a session. Read-only.
|
||||
*/
|
||||
public class ParkourJoinEvent extends EventWrapper {
|
||||
|
||||
public final ParkourUser player;
|
||||
|
||||
public ParkourJoinEvent(ParkourUser player) {
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.event.EventWrapper;
|
||||
|
||||
/**
|
||||
* Gets called when a player leaves a session. Read-only.
|
||||
*/
|
||||
public class ParkourLeaveEvent extends EventWrapper {
|
||||
|
||||
public final ParkourUser player;
|
||||
|
||||
public ParkourLeaveEvent(ParkourUser player) {
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.efnilite.vilib.event.EventWrapper;
|
||||
import dev.efnilite.vilib.schematic.Schematic;
|
||||
|
||||
/**
|
||||
* Gets called when a new jump is generated. Read-only.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class ParkourSchematicGenerateEvent extends EventWrapper {
|
||||
|
||||
public final Schematic schematic;
|
||||
public final ParkourGenerator generator;
|
||||
public final ParkourPlayer player;
|
||||
|
||||
public ParkourSchematicGenerateEvent(Schematic schematic, ParkourGenerator generator, ParkourPlayer player) {
|
||||
this.schematic = schematic;
|
||||
this.generator = generator;
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.efnilite.vilib.event.EventWrapper;
|
||||
|
||||
/**
|
||||
* Gets called when a point is scored. Read-only.
|
||||
*/
|
||||
public class ParkourScoreEvent extends EventWrapper {
|
||||
|
||||
public final ParkourPlayer player;
|
||||
|
||||
public ParkourScoreEvent(ParkourPlayer player) {
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.efnilite.vilib.event.EventWrapper;
|
||||
|
||||
/**
|
||||
* Gets called when a player starts spectating a session. Read-only.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class ParkourSpectateEvent extends EventWrapper {
|
||||
|
||||
public final ParkourSpectator player;
|
||||
|
||||
public ParkourSpectateEvent(ParkourSpectator player) {
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
199
src/main/java/dev/loki/loparkour/config/Config.java
Normal file
199
src/main/java/dev/loki/loparkour/config/Config.java
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.schematic.Schematics;
|
||||
import dev.efnilite.vilib.configupdater.ConfigUpdater;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Config management class.
|
||||
*/
|
||||
public enum Config {
|
||||
|
||||
CONFIG("config.yml", List.of("styles")),
|
||||
GENERATION("generation.yml", null),
|
||||
REWARDS("rewards-v2.yml", List.of("score-rewards", "interval-rewards", "one-time-rewards")),
|
||||
SCHEMATICS("schematics/schematics.yml", List.of("difficulty"));
|
||||
|
||||
/**
|
||||
* The path to this file, incl. plugin folder.
|
||||
*/
|
||||
public final File path;
|
||||
/**
|
||||
* The name of this file, e.g. config.yml
|
||||
*/
|
||||
public final String fileName;
|
||||
/**
|
||||
* The sections in the file that will be ignored when updating the keys.
|
||||
*/
|
||||
public final List<String> ignoredSections;
|
||||
/**
|
||||
* The {@link FileConfiguration} instance associated with this config file.
|
||||
*/
|
||||
public FileConfiguration fileConfiguration;
|
||||
|
||||
Config(String fileName, @Nullable List<String> ignoredSections) {
|
||||
this.fileName = fileName;
|
||||
this.ignoredSections = ignoredSections;
|
||||
this.path = LoParkour.getInFolder(fileName);
|
||||
|
||||
if (!path.exists()) {
|
||||
LoParkour.getPlugin().saveResource(fileName, false);
|
||||
}
|
||||
|
||||
update();
|
||||
load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all config files.
|
||||
*/
|
||||
public static void reload(boolean initialLoad) {
|
||||
for (Config config : values()) {
|
||||
config.load();
|
||||
}
|
||||
|
||||
// read config stuff
|
||||
Rewards.init();
|
||||
Locales.init();
|
||||
Schematics.init();
|
||||
Option.init(initialLoad);
|
||||
|
||||
LoParkour.log("Loaded all config files");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the file from disk.
|
||||
*/
|
||||
public void load() {
|
||||
this.fileConfiguration = YamlConfiguration.loadConfiguration(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the file so all keys are present.
|
||||
*/
|
||||
public void update() {
|
||||
try {
|
||||
ConfigUpdater.update(LoParkour.getPlugin(), fileName, path, ignoredSections);
|
||||
} catch (Exception ex) {
|
||||
LoParkour.logging().stack("Error while trying to update config file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return True when path exists, false if not.
|
||||
*/
|
||||
public boolean isPath(@NotNull String path) {
|
||||
return fileConfiguration.isSet(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The value at path.
|
||||
*/
|
||||
public Object get(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.get(path);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The boolean value at path.
|
||||
*/
|
||||
public boolean getBoolean(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getBoolean(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The int value at path.
|
||||
*/
|
||||
public int getInt(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getInt(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The double value at path.
|
||||
*/
|
||||
public double getDouble(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getDouble(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The String value at path.
|
||||
*/
|
||||
@NotNull
|
||||
public String getString(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getString(path, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The String list value at path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getStringList(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getStringList(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The int list value at path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<Integer> getIntList(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getIntegerList(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @param deep Whether search should include children of children as well.
|
||||
* @return The children nodes from path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getChildren(@NotNull String path, boolean... deep) {
|
||||
check(path);
|
||||
|
||||
ConfigurationSection section = fileConfiguration.getConfigurationSection(path);
|
||||
|
||||
if (section == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return new ArrayList<>(section.getKeys(deep != null));
|
||||
}
|
||||
|
||||
// checks if the specified path exists to avoid developer error
|
||||
private void check(@NotNull String path) {
|
||||
if (!isPath(path)) {
|
||||
throw new NoSuchElementException("Unknown path %s in %s".formatted(path, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
257
src/main/java/dev/loki/loparkour/config/Locales.java
Normal file
257
src/main/java/dev/loki/loparkour/config/Locales.java
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Locale message/item handler.
|
||||
*/
|
||||
public class Locales {
|
||||
|
||||
// a list of all nodes, used to check against missing nodes
|
||||
private static List<String> resourceNodes;
|
||||
|
||||
/**
|
||||
* A map of all locales with their respective yml trees
|
||||
*/
|
||||
public static final Map<String, FileConfiguration> locales = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Initializes this Locale handler.
|
||||
*/
|
||||
public static void init() {
|
||||
Plugin plugin = LoParkour.getPlugin();
|
||||
|
||||
Task.create(plugin).async().execute(() -> {
|
||||
locales.clear();
|
||||
|
||||
FileConfiguration embedded = YamlConfiguration.loadConfiguration(new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8));
|
||||
|
||||
// get all nodes from the plugin's english resource, aka the most updated version
|
||||
resourceNodes = getChildren(embedded, "", true);
|
||||
|
||||
File folder = LoParkour.getInFolder("locales");
|
||||
|
||||
// download files to locales folder
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs();
|
||||
}
|
||||
|
||||
String[] files = folder.list();
|
||||
|
||||
// create non-existent files
|
||||
if (files != null && files.length == 0) {
|
||||
plugin.saveResource("locales/en.yml", false);
|
||||
plugin.saveResource("locales/nl.yml", false);
|
||||
plugin.saveResource("locales/fr.yml", false);
|
||||
plugin.saveResource("locales/zh_cn.yml", false);
|
||||
}
|
||||
|
||||
// get all files in locales folder
|
||||
try (Stream<Path> stream = Files.list(folder.toPath())) {
|
||||
stream.forEach(path -> {
|
||||
File file = path.toFile();
|
||||
|
||||
// get locale from file name
|
||||
String locale = file.getName().split("\\.")[0];
|
||||
|
||||
LoParkour.log("Found locale " + locale);
|
||||
|
||||
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||
validate(embedded, config, file);
|
||||
|
||||
locales.put(locale, config);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
LoParkour.logging().stack("Error while trying to read locale files", "restart/reload your server", ex);
|
||||
}
|
||||
}).run();
|
||||
}
|
||||
|
||||
// validates whether a lang file contains all required keys.
|
||||
// if it doesn't, automatically add them
|
||||
private static void validate(FileConfiguration provided, FileConfiguration user, File localPath) {
|
||||
List<String> userNodes = getChildren(user, "", true);
|
||||
|
||||
for (String node : resourceNodes) {
|
||||
if (userNodes.contains(node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LoParkour.log("Fixing missing config node %s in %s".formatted(node, localPath.getName()));
|
||||
|
||||
user.set(node, provided.get(node));
|
||||
}
|
||||
|
||||
LoParkour.log("Validated locale " + localPath.getName());
|
||||
|
||||
try {
|
||||
user.save(localPath);
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while trying to save fixed config file %s".formatted(localPath), "delete this file and restart your server", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a String from the provided path in the provided player's locale.
|
||||
* If the player is a {@link ParkourUser}, their locale value will be used.
|
||||
* If not, the default locale will be used.
|
||||
*
|
||||
* @param player The player
|
||||
* @param path The path
|
||||
* @return a String
|
||||
*/
|
||||
public static String getString(Player player, String path) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
|
||||
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
|
||||
return getString(locale, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a coloured String from the provided path in the provided locale file
|
||||
*
|
||||
* @param locale The locale
|
||||
* @param path The path
|
||||
* @return a String
|
||||
*/
|
||||
public static String getString(String locale, String path) {
|
||||
return getValue(locale, config -> config.getString(path), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an uncoloured String list from the provided path in the provided locale file
|
||||
*
|
||||
* @param locale The locale
|
||||
* @param path The path
|
||||
* @return a String list
|
||||
*/
|
||||
public static List<String> getStringList(String locale, String path) {
|
||||
return getValue(locale, config -> config.getStringList(path), Collections.emptyList());
|
||||
}
|
||||
|
||||
private static <T> T getValue(String locale, Function<FileConfiguration, T> f, T def) {
|
||||
if (locales.isEmpty()) {
|
||||
return def;
|
||||
}
|
||||
|
||||
FileConfiguration config = locales.get(locale);
|
||||
|
||||
return config != null ? f.apply(config) : def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an item from a json locale file.
|
||||
* The locale is derived from the player.
|
||||
* If the player is a {@link ParkourUser}, their locale value will be used.
|
||||
* If not, the default locale will be used.
|
||||
*
|
||||
* @param player The player
|
||||
* @param path The full path of the item in the locale file
|
||||
* @return a non-null {@link Item} instance built from the description in the locale file
|
||||
*/
|
||||
@NotNull
|
||||
public static Item getItem(@NotNull Player player, String path, String... replace) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
|
||||
return getItem(locale, path, replace);
|
||||
}
|
||||
|
||||
private static final Pattern pattern = Pattern.compile("%[a-z]");
|
||||
|
||||
/**
|
||||
* Returns an item from a provided json locale file with possible replacements.
|
||||
*
|
||||
* @param locale The locale
|
||||
* @param path The path in the json file
|
||||
* @param replace The Strings that will replace any appearances of a String following the regex "%[a-z]"
|
||||
* @return a non-null {@link Item} instance built from the description in the locale file
|
||||
*/
|
||||
@NotNull
|
||||
public static Item getItem(String locale, String path, String... replace) {
|
||||
if (locales.isEmpty()) { // during reloading
|
||||
return new Item(Material.STONE, "");
|
||||
}
|
||||
|
||||
FileConfiguration base = locales.get(locale);
|
||||
|
||||
String material = base.getString("%s.material".formatted(path));
|
||||
String name = base.getString("%s.name".formatted(path));
|
||||
String lore = base.getString("%s.lore".formatted(path));
|
||||
int modelId = base.getInt("%s.model_id".formatted(path), -1);
|
||||
|
||||
if (material == null) {
|
||||
material = "";
|
||||
}
|
||||
if (name == null) {
|
||||
name = "";
|
||||
}
|
||||
if (lore == null) {
|
||||
lore = "";
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
Matcher matcher = pattern.matcher(name);
|
||||
while (matcher.find()) {
|
||||
if (idx == replace.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
name = name.replaceFirst(matcher.group(), replace[idx]);
|
||||
idx++;
|
||||
}
|
||||
|
||||
matcher = pattern.matcher(lore);
|
||||
|
||||
while (matcher.find()) {
|
||||
if (idx == replace.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
lore = lore.replaceFirst(matcher.group(), replace[idx]);
|
||||
idx++;
|
||||
}
|
||||
|
||||
Item item = new Item(Material.getMaterial(material.toUpperCase()), name);
|
||||
|
||||
if (!lore.isEmpty()) {
|
||||
item.lore(lore.split("\\|\\|"));
|
||||
}
|
||||
|
||||
if (modelId != -1) {
|
||||
item.modelId(modelId);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public static List<String> getChildren(FileConfiguration file, String path, boolean deep) {
|
||||
ConfigurationSection section = file.getConfigurationSection(path);
|
||||
|
||||
return section != null ? new ArrayList<>(section.getKeys(deep)) : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
264
src/main/java/dev/loki/loparkour/config/Option.java
Normal file
264
src/main/java/dev/loki/loparkour/config/Option.java
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.style.RandomStyle;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.efnilite.vilib.particle.ParticleData;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Class for variables required in generating without accessing the file a lot (constants)
|
||||
*/
|
||||
public class Option {
|
||||
|
||||
public static double BORDER_SIZE;
|
||||
public static List<Integer> POSSIBLE_LEADS;
|
||||
|
||||
// Advanced settings
|
||||
public static BlockFace HEADING;
|
||||
|
||||
public static Map<ParkourOption, Boolean> OPTIONS_ENABLED;
|
||||
public static Map<ParkourOption, String> OPTIONS_DEFAULTS;
|
||||
|
||||
public static Location GO_BACK_LOC;
|
||||
|
||||
public static void init(boolean firstLoad) {
|
||||
initSql();
|
||||
initEnums();
|
||||
initGeneration();
|
||||
initStyles("styles.list", Config.CONFIG.fileConfiguration, RandomStyle::new)
|
||||
.forEach(Registry::register);
|
||||
|
||||
GO_BACK_LOC = parseLocation(Config.CONFIG.getString("bungeecord.go-back"));
|
||||
String[] axes = Config.CONFIG.getString("bungeecord.go-back-axes").split(",");
|
||||
GO_BACK_LOC.setPitch(Float.parseFloat(axes[0]));
|
||||
GO_BACK_LOC.setYaw(Float.parseFloat(axes[1]));
|
||||
|
||||
// General settings
|
||||
|
||||
// Options
|
||||
|
||||
List<ParkourOption> options = new ArrayList<>(Arrays.asList(ParkourOption.values()));
|
||||
|
||||
// exceptions
|
||||
options.remove(ParkourOption.JOIN);
|
||||
options.remove(ParkourOption.ADMIN);
|
||||
|
||||
// =====================================
|
||||
|
||||
OPTIONS_DEFAULTS = new HashMap<>();
|
||||
OPTIONS_ENABLED = new HashMap<>();
|
||||
|
||||
String prefix = "default-values";
|
||||
for (ParkourOption option : options) {
|
||||
String parent = "%s.%s".formatted(prefix, option.path);
|
||||
|
||||
// register enabled value
|
||||
OPTIONS_ENABLED.put(option, Config.CONFIG.getBoolean("%s.enabled".formatted(parent)));
|
||||
|
||||
// register default value
|
||||
if (!Config.CONFIG.isPath("%s.default".formatted(parent))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = Config.CONFIG.get("%s.default".formatted(parent));
|
||||
|
||||
if (value != null) {
|
||||
OPTIONS_DEFAULTS.put(option, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================
|
||||
|
||||
// Config stuff
|
||||
|
||||
POSSIBLE_LEADS = Config.CONFIG.getIntList("options.leads.amount");
|
||||
for (int lead : new ArrayList<>(POSSIBLE_LEADS)) {
|
||||
if (lead < 1 || lead > 128) {
|
||||
LoParkour.logging().error("Invalid lead: %d. Should be above 1 and below 128.".formatted(lead));
|
||||
POSSIBLE_LEADS.remove((Object) lead);
|
||||
}
|
||||
}
|
||||
|
||||
// Generation
|
||||
String heading = Config.GENERATION.getString("advanced.island.parkour.heading");
|
||||
|
||||
switch (heading.toLowerCase()) {
|
||||
case "north" -> HEADING = BlockFace.NORTH;
|
||||
case "south" -> HEADING = BlockFace.SOUTH;
|
||||
case "west" -> HEADING = BlockFace.WEST;
|
||||
case "east" -> HEADING = BlockFace.EAST;
|
||||
default -> LoParkour.logging().error("Invalid heading: %s".formatted(heading));
|
||||
}
|
||||
|
||||
// Scoring
|
||||
|
||||
if (firstLoad) {
|
||||
BORDER_SIZE = Config.GENERATION.getDouble("advanced.border-size");
|
||||
SQL = Config.CONFIG.getBoolean("sql.enabled");
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector stringToVector(String direction) {
|
||||
return switch (direction.toLowerCase()) {
|
||||
case "north" -> new Vector(0, 0, -1);
|
||||
case "south" -> new Vector(0, 0, 1);
|
||||
case "west" -> new Vector(-1, 0, 0);
|
||||
default -> new Vector(1, 0, 0); // east
|
||||
};
|
||||
}
|
||||
|
||||
private static Location parseLocation(String location) {
|
||||
String[] values = location.replaceAll("[()]", "").replaceAll("[, ]", " ").split(" ");
|
||||
|
||||
World world = Bukkit.getWorld(values[3]);
|
||||
|
||||
if (world == null) {
|
||||
world = Bukkit.getWorlds().get(0);
|
||||
}
|
||||
|
||||
return new Location(world, Double.parseDouble(values[0]), Double.parseDouble(values[1]), Double.parseDouble(values[2]));
|
||||
}
|
||||
|
||||
public static ParticleShape PARTICLE_SHAPE;
|
||||
public static Sound SOUND_TYPE;
|
||||
public static int SOUND_PITCH;
|
||||
public static int SOUND_VOLUME;
|
||||
public static Particle PARTICLE_TYPE;
|
||||
public static ParticleData<?> PARTICLE_DATA;
|
||||
|
||||
private static void initEnums() {
|
||||
String value = Config.CONFIG.getString("particles.sound-type").toUpperCase();
|
||||
|
||||
try {
|
||||
SOUND_TYPE = Sound.valueOf(value);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
SOUND_TYPE = Sound.valueOf("BLOCK_NOTE_PLING");
|
||||
LoParkour.logging().error("Invalid sound: %s".formatted(value));
|
||||
}
|
||||
|
||||
value = Config.CONFIG.getString("particles.particle-type");
|
||||
try {
|
||||
PARTICLE_TYPE = Particle.valueOf(value);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
PARTICLE_TYPE = Particle.valueOf("SPELL_INSTANT");
|
||||
LoParkour.logging().error("Invalid particle type: %s".formatted(value));
|
||||
}
|
||||
|
||||
SOUND_PITCH = Config.CONFIG.getInt("particles.sound-pitch");
|
||||
SOUND_VOLUME = Config.CONFIG.getInt("particles.sound-volume");
|
||||
PARTICLE_SHAPE = ParticleShape.valueOf(Config.CONFIG.getString("particles.particle-shape").toUpperCase());
|
||||
PARTICLE_DATA = new ParticleData<>(PARTICLE_TYPE, null, 10, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public enum ParticleShape {
|
||||
DOT, CIRCLE, BOX
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// MySQL
|
||||
public static boolean SQL;
|
||||
public static int SQL_PORT;
|
||||
public static String SQL_URL;
|
||||
public static String SQL_DB;
|
||||
public static String SQL_USERNAME;
|
||||
public static String SQL_PASSWORD;
|
||||
public static String SQL_PREFIX;
|
||||
|
||||
private static void initSql() {
|
||||
SQL_PORT = Config.CONFIG.getInt("sql.port");
|
||||
SQL_DB = Config.CONFIG.getString("sql.database");
|
||||
SQL_URL = Config.CONFIG.getString("sql.url");
|
||||
SQL_USERNAME = Config.CONFIG.getString("sql.username");
|
||||
SQL_PASSWORD = Config.CONFIG.getString("sql.password");
|
||||
SQL_PREFIX = Config.CONFIG.getString("sql.prefix");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// Generation
|
||||
|
||||
public static double TYPE_NORMAL;
|
||||
public static double TYPE_SPECIAL;
|
||||
public static double TYPE_SCHEMATICS;
|
||||
|
||||
public static double SPECIAL_ICE;
|
||||
public static double SPECIAL_SLAB;
|
||||
public static double SPECIAL_PANE;
|
||||
public static double SPECIAL_FENCE;
|
||||
|
||||
public static double NORMAL_DISTANCE_1;
|
||||
public static double NORMAL_DISTANCE_2;
|
||||
public static double NORMAL_DISTANCE_3;
|
||||
public static double NORMAL_DISTANCE_4;
|
||||
|
||||
public static double NORMAL_HEIGHT_1;
|
||||
public static double NORMAL_HEIGHT_0;
|
||||
public static double NORMAL_HEIGHT_NEG1;
|
||||
public static double NORMAL_HEIGHT_NEG2;
|
||||
|
||||
public static int MAX_Y;
|
||||
public static int MIN_Y;
|
||||
|
||||
private static void initGeneration() {
|
||||
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
|
||||
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.schematic") / 100.0;
|
||||
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.special") / 100.0;
|
||||
|
||||
SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / 100.0;
|
||||
SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / 100.0;
|
||||
SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / 100.0;
|
||||
SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / 100.0;
|
||||
|
||||
NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / 100.0;
|
||||
NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / 100.0;
|
||||
NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / 100.0;
|
||||
NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / 100.0;
|
||||
|
||||
NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / 100.0;
|
||||
NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / 100.0;
|
||||
NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / 100.0;
|
||||
NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / 100.0;
|
||||
|
||||
MAX_Y = Config.GENERATION.getInt("generation.settings.max-y");
|
||||
MIN_Y = Config.GENERATION.getInt("generation.settings.min-y");
|
||||
|
||||
if (MIN_Y >= MAX_Y) {
|
||||
MIN_Y = 100;
|
||||
MAX_Y = 200;
|
||||
|
||||
LoParkour.logging().stack("Provided minimum y is the same or larger than maximum y!", "check your generation.yml file");
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
|
||||
public static Set<Style> initStyles(String path, FileConfiguration config, BiFunction<String, List<Material>, Style> fn) {
|
||||
var styles = new HashSet<Style>();
|
||||
|
||||
for (String style : Locales.getChildren(config, path, false)) {
|
||||
styles.add(fn.apply(style,
|
||||
config.getStringList("%s.%s".formatted(path, style)).stream()
|
||||
.map(name -> {
|
||||
var material = Material.getMaterial(name.toUpperCase());
|
||||
|
||||
if (material == null) {
|
||||
LoParkour.logging().error("Invalid material %s in style %s".formatted(name, style));
|
||||
return Material.STONE;
|
||||
}
|
||||
|
||||
return material;
|
||||
})
|
||||
.toList()));
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
/**
|
||||
* Options for {@link ParkourGenerator}s.
|
||||
*/
|
||||
public enum GeneratorOption {
|
||||
|
||||
DISABLE_SCHEMATICS,
|
||||
DISABLE_SPECIAL,
|
||||
REDUCE_RANDOM_BLOCK_SELECTION_ANGLE
|
||||
|
||||
}
|
||||
89
src/main/java/dev/loki/loparkour/generator/Island.java
Normal file
89
src/main/java/dev/loki/loparkour/generator/Island.java
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.schematic.Schematic;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Spawn island handler.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public final class Island {
|
||||
|
||||
/**
|
||||
* The session.
|
||||
*/
|
||||
public final Session session;
|
||||
/**
|
||||
* The schematic.
|
||||
*/
|
||||
public final Schematic schematic;
|
||||
|
||||
/**
|
||||
* The blocks that have been affected by the schematic.
|
||||
*/
|
||||
public List<Block> blocks;
|
||||
|
||||
public Island(@NotNull Session session, @Nullable Schematic schematic) {
|
||||
this.session = session;
|
||||
this.schematic = schematic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the island and teleports the player.
|
||||
*/
|
||||
public void build(Location location) {
|
||||
if (schematic == null) {
|
||||
return;
|
||||
}
|
||||
LoParkour.log("Building island");
|
||||
|
||||
blocks = schematic.paste(location.subtract(0, schematic.getDimensions().getY(), 0));
|
||||
|
||||
Material playerMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.spawn.player-block").toUpperCase());
|
||||
Material parkourMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.parkour.begin-block").toUpperCase());
|
||||
|
||||
try {
|
||||
Block player = blocks.stream().filter(block -> block.getType() == playerMaterial).findAny().orElseThrow();
|
||||
Block parkour = blocks.stream().filter(block -> block.getType() == parkourMaterial).findAny().orElseThrow();
|
||||
|
||||
player.setType(Material.AIR);
|
||||
parkour.setType(Material.AIR);
|
||||
|
||||
Location ps = player.getLocation().add(0.5, 0, 0.5);
|
||||
ps.setYaw(Config.GENERATION.getInt("advanced.island.spawn.yaw"));
|
||||
ps.setPitch(Config.GENERATION.getInt("advanced.island.spawn.pitch"));
|
||||
|
||||
session.generator.generateFirst(ps, parkour.getLocation().subtract(session.generator.heading).subtract(0, 1, 0));
|
||||
session.generator.startTick();
|
||||
session.getPlayers().forEach(pp -> pp.setup(ps));
|
||||
} catch (NoSuchElementException ex) {
|
||||
LoParkour.logging().stack("Error while trying to find parkour or player spawn in schematic %s".formatted(schematic.getFile().getName()),
|
||||
"check if you used the same material as the one in generation.yml", ex);
|
||||
|
||||
blocks.forEach(block -> block.setType(Material.AIR));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the island.
|
||||
*/
|
||||
public void destroy() {
|
||||
if (blocks == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
blocks.forEach(block -> block.setType(Material.AIR, false));
|
||||
}
|
||||
}
|
||||
137
src/main/java/dev/loki/loparkour/generator/JumpDirector.java
Normal file
137
src/main/java/dev/loki/loparkour/generator/JumpDirector.java
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* The JumpDirector provides calculations to ensure parkour blocks stay within the allowed playable area.
|
||||
*/
|
||||
public class JumpDirector {
|
||||
|
||||
private static final int SAFE_DISTANCE = 5;
|
||||
private final BoundingBox bb;
|
||||
private final double[][] progress;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param bb The bounding box of the allowed parkour area.
|
||||
* @param point The point to base the direction on.
|
||||
*/
|
||||
public JumpDirector(@NotNull BoundingBox bb, @NotNull Vector point) {
|
||||
this.bb = bb;
|
||||
this.progress = calculateParameterization(point);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the player's position in a parameter form, to make it easier to detect when the player is near the edge of the border.
|
||||
* Returns a 2-dimensional array where the first array index is used to select the x, y and z (0, 1 and 2 respectively).
|
||||
*
|
||||
* @return Array where the first index is tx and second index is borderMarginX (see comments in code for explanation).
|
||||
*/
|
||||
private double[][] calculateParameterization(@NotNull Vector point) {
|
||||
Vector min = bb.getMin();
|
||||
Vector max = bb.getMax();
|
||||
|
||||
// the total dimensions
|
||||
double dx = max.getX() - min.getX();
|
||||
double dy = max.getY() - min.getY();
|
||||
double dz = max.getZ() - min.getZ();
|
||||
|
||||
// the relative x, y and z coordinates
|
||||
// relative being from the min point of the selection zone
|
||||
double relativeX = point.getX() - min.getX();
|
||||
double relativeY = point.getY() - min.getY();
|
||||
double relativeZ = point.getZ() - min.getZ();
|
||||
|
||||
// get progress along axes
|
||||
// tx = 0 means that the player is at the same x coordinate as the min point (origin)
|
||||
// tx = 1 means that the player is at the same x coordinate as the max point
|
||||
// everything between is the progress between these two points, relatively speaking
|
||||
double tx = relativeX / dx;
|
||||
double ty = relativeY / dy;
|
||||
double tz = relativeZ / dz;
|
||||
|
||||
// the margin until the border
|
||||
// if tx < borderMarginX, it means the x coordinate is within 'safeDistance' blocks of the border
|
||||
double borderMarginX = SAFE_DISTANCE / dx;
|
||||
double borderMarginY = SAFE_DISTANCE / dy;
|
||||
double borderMarginZ = SAFE_DISTANCE / dz;
|
||||
|
||||
return new double[][]{{tx, borderMarginX}, {ty, borderMarginY}, {tz, borderMarginZ}};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the heading to make sure it avoids the border of the selected zone.
|
||||
* When the most recent block is detected to be within a 5-block radius of the border,
|
||||
* the heading will automatically be turned around to ensure that the edge does not get
|
||||
* destroyed.
|
||||
*
|
||||
* @return The recommended new heading. Current heading if no modification is needed.
|
||||
*/
|
||||
@NotNull
|
||||
public Vector getRecommendedHeading(Vector current) {
|
||||
// get x values from progress array
|
||||
double tx = progress[0][0];
|
||||
double borderMarginX = progress[0][1];
|
||||
|
||||
// get z values from progress array
|
||||
double tz = progress[2][0];
|
||||
double borderMarginZ = progress[2][1];
|
||||
|
||||
Vector recommendedHeading = new Vector(0, 0, 0);
|
||||
// check border
|
||||
if (tx < borderMarginX) {
|
||||
// x should increase
|
||||
recommendedHeading = new Vector(1, 0, 1);
|
||||
} else if (tx > 1 - borderMarginX) {
|
||||
// x should decrease
|
||||
recommendedHeading = new Vector(-1, 0, -1);
|
||||
}
|
||||
|
||||
if (tz < borderMarginZ) {
|
||||
// z should increase
|
||||
recommendedHeading = new Vector(1, 0, 1);
|
||||
} else if (tz > 1 - borderMarginZ) {
|
||||
// z should decrease
|
||||
recommendedHeading = new Vector(-1, 0, -1);
|
||||
}
|
||||
|
||||
if (recommendedHeading.lengthSquared() == 0) {
|
||||
return current;
|
||||
} else {
|
||||
return recommendedHeading;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the height to make sure the player doesn't go below the playable zone.
|
||||
* If the current height is fine, it will return 0.
|
||||
* If the current height is within the border margin, it will return a value (1 or -1)
|
||||
* to make sure the player doesn't go below this value
|
||||
*
|
||||
* @return The recommended new height. 0 if no modification is needed.
|
||||
*/
|
||||
public int getRecommendedHeight(int current) {
|
||||
double ty = progress[1][0];
|
||||
double borderMarginY = progress[1][1];
|
||||
|
||||
if (ty < borderMarginY) {
|
||||
// y should increase
|
||||
return 1;
|
||||
} else if (ty > 1 - borderMarginY) {
|
||||
// y should decrease
|
||||
return -1;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The progress array.
|
||||
* @see #calculateParameterization(Vector)
|
||||
*/
|
||||
public double[][] getProgress() {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import dev.efnilite.vilib.util.Probs;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Generates a random offset (the sidestep) for a parkour block.
|
||||
* Source: <a href="https://i.imgur.com/RPevMfX.png">here</a>.
|
||||
*/
|
||||
public class JumpOffsetGenerator {
|
||||
|
||||
private final int maxOffset;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param y The height difference.
|
||||
*/
|
||||
public JumpOffsetGenerator(int y, int distance) {
|
||||
this.maxOffset = switch (y) {
|
||||
case 1 -> switch (distance) {
|
||||
case 1 -> 4;
|
||||
case 2 -> 3;
|
||||
default -> 2;
|
||||
};
|
||||
case 0 -> switch (distance) {
|
||||
case 1, 2 -> 4;
|
||||
case 3 -> 3;
|
||||
default -> 2;
|
||||
};
|
||||
case -1 -> switch (distance) {
|
||||
case 1, 2 -> 5;
|
||||
case 3 -> 4;
|
||||
default -> 3;
|
||||
};
|
||||
case -2 -> switch (distance) {
|
||||
case 1, 2 -> 5;
|
||||
default -> 4;
|
||||
};
|
||||
default -> throw new IllegalArgumentException("Unknown jump height difference %d".formatted(y));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mean The mean (average) value for the offset. Usually 0 to avoid parkour going only left or right.
|
||||
* @param standardDeviation The standard deviation.
|
||||
* @param random The random instance.
|
||||
* @return A random jump-able offset.
|
||||
*/
|
||||
public int getRandomOffset(double mean, double standardDeviation, Random random) {
|
||||
Map<Integer, Double> distribution = Colls.range(-maxOffset, maxOffset + 1)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(offset -> offset, offset -> Probs.normalpdf(mean, standardDeviation, offset)));
|
||||
|
||||
return Probs.random(distribution, random);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mean The mean (average) value for the offset. Usually 0 to avoid parkour going only left or right.
|
||||
* @param standardDeviation The standard deviation.
|
||||
* @return A random jump-able offset.
|
||||
*/
|
||||
public int getRandomOffset(double mean, double standardDeviation) {
|
||||
return getRandomOffset(mean, standardDeviation, ThreadLocalRandom.current());
|
||||
}
|
||||
}
|
||||
843
src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java
Normal file
843
src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java
Normal file
|
|
@ -0,0 +1,843 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourFallEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import dev.efnilite.vilib.particle.ParticleData;
|
||||
import dev.efnilite.vilib.particle.Particles;
|
||||
import dev.efnilite.vilib.schematic.Schematic;
|
||||
import dev.efnilite.vilib.schematic.Schematics;
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import dev.efnilite.vilib.util.Locations;
|
||||
import dev.efnilite.vilib.util.Probs;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Fence;
|
||||
import org.bukkit.block.data.type.GlassPane;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The class that generates the parkour, which each {@link ParkourPlayer} has.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class ParkourGenerator {
|
||||
|
||||
/**
|
||||
* The amount of blocks that trail behind the player.
|
||||
*/
|
||||
public static final int BLOCK_TRAIL = 2;
|
||||
|
||||
/**
|
||||
* This generator's score
|
||||
*/
|
||||
public int score = 0;
|
||||
|
||||
/**
|
||||
* The total score achieved in this Generator instance
|
||||
*/
|
||||
public int totalScore = 0;
|
||||
|
||||
/**
|
||||
* The schematic cooldown
|
||||
*/
|
||||
public int schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
|
||||
/**
|
||||
* Whether this generator has been stopped
|
||||
*/
|
||||
public boolean stopped = false;
|
||||
|
||||
/**
|
||||
* The zone in which the parkour can take place. (playable area)
|
||||
*/
|
||||
public Location[] zone;
|
||||
|
||||
/**
|
||||
* The player
|
||||
*/
|
||||
public ParkourPlayer player;
|
||||
|
||||
/**
|
||||
* The task used in checking the player's current location
|
||||
*/
|
||||
public BukkitTask task;
|
||||
|
||||
/**
|
||||
* Where blocks from schematics spawn
|
||||
*/
|
||||
public Location blockSpawn;
|
||||
|
||||
/**
|
||||
* Where the player spawns on reset
|
||||
*/
|
||||
public Location playerSpawn;
|
||||
|
||||
/**
|
||||
* Instant when run started.
|
||||
*/
|
||||
public Instant start;
|
||||
|
||||
/**
|
||||
* The direction of the parkour
|
||||
*/
|
||||
public Vector heading = Option.HEADING.getDirection();
|
||||
|
||||
/**
|
||||
* Generator options
|
||||
*/
|
||||
public final List<GeneratorOption> generatorOptions;
|
||||
|
||||
/**
|
||||
* The {@link Session} associated with this Generator.
|
||||
*/
|
||||
public final Session session;
|
||||
|
||||
/**
|
||||
* This Generator's {@link Profile}.
|
||||
*/
|
||||
public final Profile profile = new Profile();
|
||||
|
||||
/**
|
||||
* The island instance.
|
||||
*/
|
||||
public final Island island;
|
||||
|
||||
/**
|
||||
* The chances of which distance the jump should have
|
||||
*/
|
||||
public final Map<Integer, Double> distanceChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The chances of which height the jump should have
|
||||
*/
|
||||
public final Map<Integer, Double> heightChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The chances of which type of special jump
|
||||
*/
|
||||
public final Map<BlockData, Double> specialChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The chances of default jump types: schematic, 'special' (ice, etc.) or normal
|
||||
*/
|
||||
public final Map<JumpType, Double> defaultChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Whether the schematic should be deleted on the next jump.
|
||||
*/
|
||||
protected boolean deleteSchematic = false;
|
||||
protected boolean waitForSchematicCompletion = false;
|
||||
|
||||
/**
|
||||
* The last location the player was found standing in
|
||||
*/
|
||||
protected Location lastStandingPlayerLocation;
|
||||
|
||||
/**
|
||||
* A list of blocks from the (possibly) spawned structure
|
||||
*/
|
||||
protected List<Block> schematicBlocks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The player's current position index.
|
||||
*/
|
||||
protected int lastPositionIndexPlayer = -1;
|
||||
|
||||
/**
|
||||
* The history of generated blocks. The most recently generated block is the last item in the list.
|
||||
*/
|
||||
protected List<Block> history = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates a new ParkourGenerator instance
|
||||
*
|
||||
* @param session The session.
|
||||
* @param schematic The schematic to use for the spawn island.
|
||||
* @param generatorOptions The options.
|
||||
*/
|
||||
public ParkourGenerator(@NotNull Session session, @Nullable Schematic schematic, GeneratorOption... generatorOptions) {
|
||||
this.session = session;
|
||||
this.generatorOptions = Arrays.asList(generatorOptions);
|
||||
|
||||
player = session.getPlayers().get(0);
|
||||
island = new Island(session, schematic);
|
||||
zone = Divider.toSelection(session);
|
||||
|
||||
calculateChances();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ParkourGenerator instance.
|
||||
*
|
||||
* @param session The session.
|
||||
* @param generatorOptions The options.
|
||||
*/
|
||||
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
|
||||
this(session, Schematics.getSchematic(LoParkour.getPlugin(), "spawn-island"), generatorOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures generator preferences in profile can't be overridden by the player changing settings.
|
||||
*/
|
||||
public void overrideProfile() { }
|
||||
|
||||
/**
|
||||
* Calculates all chances for every variable.
|
||||
* Modification is possible in the generator constructor or through external map changes.
|
||||
*/
|
||||
protected void calculateChances() {
|
||||
defaultChances.clear();
|
||||
defaultChances.put(JumpType.DEFAULT, Option.TYPE_NORMAL);
|
||||
defaultChances.put(JumpType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
||||
defaultChances.put(JumpType.SPECIAL, Option.TYPE_SPECIAL);
|
||||
|
||||
heightChances.clear();
|
||||
heightChances.put(1, Option.NORMAL_HEIGHT_1);
|
||||
heightChances.put(0, Option.NORMAL_HEIGHT_0);
|
||||
heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
|
||||
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
|
||||
|
||||
distanceChances.clear();
|
||||
distanceChances.put(1, Option.NORMAL_DISTANCE_1);
|
||||
distanceChances.put(2, Option.NORMAL_DISTANCE_2);
|
||||
distanceChances.put(3, Option.NORMAL_DISTANCE_3);
|
||||
distanceChances.put(4, Option.NORMAL_DISTANCE_4);
|
||||
|
||||
specialChances.clear();
|
||||
specialChances.put(Material.PACKED_ICE.createBlockData(), Option.SPECIAL_ICE);
|
||||
specialChances.put(Material.SMOOTH_QUARTZ_SLAB.createBlockData("[type=bottom]"), Option.SPECIAL_SLAB);
|
||||
specialChances.put(Material.GLASS_PANE.createBlockData(), Option.SPECIAL_PANE);
|
||||
specialChances.put(Material.OAK_FENCE.createBlockData(), Option.SPECIAL_FENCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates particles around blocks.
|
||||
*
|
||||
* @param blocks The blocks.
|
||||
*/
|
||||
protected void particles(List<Block> blocks) {
|
||||
if (!profile.get("particles").asBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ParticleData<?> data = Option.PARTICLE_DATA;
|
||||
List<Location> locations = blocks.stream().map(Block::getLocation).toList();
|
||||
Location max = locations.stream().reduce(Locations::max).orElseThrow();
|
||||
Location min = locations.stream().reduce(Locations::min).orElseThrow();
|
||||
Location center = min.clone().add(max.clone().subtract(min));
|
||||
|
||||
// display particle
|
||||
switch (Option.PARTICLE_SHAPE) {
|
||||
case DOT -> Particles.draw(center.add(0.5, 1, 0.5), data.speed(0.4).size(20).offsetX(0.5).offsetY(1).offsetZ(0.5));
|
||||
case CIRCLE -> Particles.circle(center.add(0.5, 0.5, 0.5), data.size(5), (int) Math.sqrt(blocks.size()), 20);
|
||||
case BOX -> Particles.box(BoundingBox.of(max, min), player.player.getWorld(), data.size(1), 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates sound around blocks.
|
||||
*
|
||||
* @param blocks The blocks.
|
||||
*/
|
||||
protected void sound(List<Block> blocks) {
|
||||
if (!profile.get("sound").asBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// play sound
|
||||
getPlayers().forEach(viewer -> viewer.player.playSound(blocks.get(0).getLocation(), Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
|
||||
getSpectators().forEach(viewer -> viewer.player.playSound(blocks.get(0).getLocation(), Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
|
||||
}
|
||||
|
||||
protected BlockData selectBlockData() {
|
||||
Style style = Registry.getStyle(profile.get("style").value());
|
||||
|
||||
if (style == null) {
|
||||
profile.set("style", Registry.getStyles().stream()
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getName());
|
||||
return selectBlockData();
|
||||
}
|
||||
|
||||
return style.getNext().createBlockData();
|
||||
}
|
||||
|
||||
protected List<Block> selectBlocks() {
|
||||
int height = Probs.random(heightChances);
|
||||
int distance = Probs.random(distanceChances);
|
||||
|
||||
return List.of(selectNext(getLatest(), distance, height));
|
||||
}
|
||||
|
||||
// Selects the next block that will continue the parkour.
|
||||
// This is done by choosing a random value for the sideways movement.
|
||||
// Based on this sideways movement, a value for forward movement will be chosen.
|
||||
// This is done to ensure players are able to complete the jump.
|
||||
protected Block selectNext(Block current, int distance, int height) {
|
||||
JumpDirector director = new JumpDirector(BoundingBox.of(zone[0], zone[1]), getLatest().getLocation().toVector());
|
||||
|
||||
heading = director.getRecommendedHeading(heading);
|
||||
height = director.getRecommendedHeight(height);
|
||||
|
||||
// ensure special is possible
|
||||
switch (getLatest().getType()) {
|
||||
case SMOOTH_QUARTZ_SLAB -> height = Math.min(height, 0);
|
||||
case GLASS_PANE -> distance = Math.min(distance, 3);
|
||||
}
|
||||
|
||||
if (height > 0) {
|
||||
distance = Math.max(distance - height, 1);
|
||||
}
|
||||
|
||||
double mean = 0;
|
||||
double standardDeviation = generatorOptions.contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
|
||||
|
||||
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(mean, standardDeviation);
|
||||
|
||||
Vector offset = heading.clone()
|
||||
.multiply(distance + 1)
|
||||
.setY(height);
|
||||
if (offset.getX() == 0) {
|
||||
offset.setX(randomOffset);
|
||||
} else {
|
||||
offset.setZ(randomOffset);
|
||||
}
|
||||
|
||||
// rotate offset to match heading
|
||||
offset.rotateAroundY(angleInY(heading, Option.HEADING.getDirection()));
|
||||
|
||||
return current.getLocation().add(offset).getBlock();
|
||||
}
|
||||
|
||||
protected void score() {
|
||||
score++;
|
||||
totalScore++;
|
||||
|
||||
checkRewards();
|
||||
new ParkourScoreEvent(player).call();
|
||||
}
|
||||
|
||||
private void checkRewards() {
|
||||
if (!Rewards.REWARDS_ENABLED || score == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check generic score rewards
|
||||
if (Rewards.SCORE_REWARDS.containsKey(score)) {
|
||||
Rewards.SCORE_REWARDS.get(score).forEach(s -> s.execute(player, getMode()));
|
||||
}
|
||||
|
||||
// gets the correct type of score to check based on the config option
|
||||
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
|
||||
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
|
||||
if (intervalScore % interval != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Rewards.INTERVAL_REWARDS.get(interval).forEach(s -> s.execute(player, getMode()));
|
||||
}
|
||||
|
||||
if (Rewards.ONE_TIME_REWARDS.containsKey(score) && !player.collectedRewards.contains(Integer.toString(score))) {
|
||||
Rewards.ONE_TIME_REWARDS.get(score).forEach(s -> s.execute(player, getMode()));
|
||||
player.collectedRewards.add(Integer.toString(score));
|
||||
}
|
||||
}
|
||||
|
||||
protected void fall() {
|
||||
new ParkourFallEvent(player).call();
|
||||
reset(true);
|
||||
}
|
||||
|
||||
public void menu(ParkourPlayer player) {
|
||||
Menus.PARKOUR_SETTINGS.open(player);
|
||||
}
|
||||
|
||||
public void startTick() {
|
||||
task = Task.create(LoParkour.getPlugin())
|
||||
.repeat(1)
|
||||
.execute(this::tick)
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the check
|
||||
*/
|
||||
public void tick() {
|
||||
if (stopped) {
|
||||
task.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
getPlayers().forEach(other -> {
|
||||
updateVisualTime(other, other.selectedTime);
|
||||
other.updateScoreboard(this);
|
||||
other.player.setSaturation(20);
|
||||
});
|
||||
|
||||
getSpectators().forEach(ParkourSpectator::update);
|
||||
|
||||
if (player.getLocation().getWorld() != lastStandingPlayerLocation.getWorld()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.getLocation().subtract(lastStandingPlayerLocation).getY() < -10) { // fall check
|
||||
LoParkour.log("Player %s is falling".formatted(player.getName()));
|
||||
|
||||
fall();
|
||||
return;
|
||||
}
|
||||
|
||||
Location belowPlayer = player.getLocation().subtract(0, 1, 0);
|
||||
Block blockBelowPlayer = belowPlayer.getBlock(); // Get the block below
|
||||
|
||||
if (blockBelowPlayer.getType() == Material.AIR) {
|
||||
if (belowPlayer.subtract(0, 0.5, 0).getBlock().getType() == Material.AIR) {
|
||||
return;
|
||||
}
|
||||
blockBelowPlayer = belowPlayer.getBlock();
|
||||
}
|
||||
|
||||
if (schematicBlocks.contains(blockBelowPlayer) && blockBelowPlayer.getType() == Material.RED_WOOL && !deleteSchematic) { // Structure deletion check
|
||||
for (int i = 0; i < profile.get("schematicDifficulty").asDouble() * 15; i++) {
|
||||
score();
|
||||
}
|
||||
|
||||
waitForSchematicCompletion = false;
|
||||
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
generate(profile.get("blockLead").asInt());
|
||||
deleteSchematic = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!history.contains(blockBelowPlayer)) {
|
||||
return; // player is on an unknown block
|
||||
}
|
||||
|
||||
int currentIndex = history.indexOf(blockBelowPlayer); // current index of the player
|
||||
int deltaFromLast = currentIndex - lastPositionIndexPlayer;
|
||||
|
||||
if (deltaFromLast <= 0) { // the player is actually making progress and not going backwards (current index is higher than the previous)
|
||||
return;
|
||||
}
|
||||
|
||||
lastStandingPlayerLocation = player.getLocation();
|
||||
|
||||
int blockLead = profile.get("blockLead").asInt();
|
||||
|
||||
int deltaCurrentTotal = history.size() - currentIndex; // delta between current index and total
|
||||
if (deltaCurrentTotal <= blockLead) {
|
||||
generate(blockLead - deltaCurrentTotal); // generate the remaining amount so it will match
|
||||
}
|
||||
lastPositionIndexPlayer = currentIndex;
|
||||
|
||||
for (int i = currentIndex - BLOCK_TRAIL - 1; i >= currentIndex - 4 * BLOCK_TRAIL; i--) {
|
||||
// avoid setting beginning block to air
|
||||
if (i <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
history.get(i).setType(Material.AIR);
|
||||
}
|
||||
|
||||
deleteSchematic();
|
||||
|
||||
for (int i = 0; i < (Config.CONFIG.getBoolean("scoring.all-points") ? deltaFromLast : 1); i++) { // score the difference
|
||||
score();
|
||||
}
|
||||
|
||||
if (start == null) { // start stopwatch when first point is achieved
|
||||
start = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
// updates the player time
|
||||
protected void updateVisualTime(ParkourPlayer player, int selectedTime) {
|
||||
int newTime = 18000 + selectedTime;
|
||||
if (newTime >= 24000) {
|
||||
newTime -= 24000;
|
||||
}
|
||||
|
||||
player.player.setPlayerTime(newTime, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the parkour. If regenerate is false, this generator is stopped and the island is destroyed.
|
||||
*
|
||||
* @param regenerate True if parkour should regenerate, false if not.
|
||||
*/
|
||||
public void reset(boolean regenerate) {
|
||||
LoParkour.log("Resetting generator, regenerate = %s".formatted(regenerate));
|
||||
|
||||
stopped = !regenerate;
|
||||
|
||||
if (!regenerate && task == null) {
|
||||
LoParkour.logging().warn("## Incomplete joining setup.");
|
||||
LoParkour.logging().warn("## There has probably been an error somewhere. Please report this error!");
|
||||
LoParkour.logging().warn("## You don't have to report this warning.");
|
||||
}
|
||||
|
||||
lastPositionIndexPlayer = 0;
|
||||
if (!history.isEmpty()) {
|
||||
history.remove(0);
|
||||
history.forEach(block -> block.setType(Material.AIR, false));
|
||||
history.clear();
|
||||
}
|
||||
|
||||
waitForSchematicCompletion = false;
|
||||
deleteSchematic = true;
|
||||
deleteSchematic();
|
||||
|
||||
Leaderboard leaderboard = getMode().getLeaderboard();
|
||||
int record = leaderboard != null ? leaderboard.get(player.getUUID()).score() : 0;
|
||||
|
||||
if (profile.get("showFallMessage").asBoolean()) {
|
||||
String message;
|
||||
int number = 0;
|
||||
|
||||
if (score == record) {
|
||||
message = "settings.parkour_settings.items.fall_message.formats.tied";
|
||||
} else if (score > record) {
|
||||
number = score - record;
|
||||
message = "settings.parkour_settings.items.fall_message.formats.beat";
|
||||
} else {
|
||||
number = record - score;
|
||||
message = "settings.parkour_settings.items.fall_message.formats.miss";
|
||||
}
|
||||
|
||||
for (ParkourPlayer players : getPlayers()) {
|
||||
players.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
||||
players.sendTranslated("settings.parkour_settings.items.fall_message.score", Integer.toString(score));
|
||||
players.sendTranslated("settings.parkour_settings.items.fall_message.time", getFormattedTime());
|
||||
players.sendTranslated("settings.parkour_settings.items.fall_message.high_score", Integer.toString(record));
|
||||
players.sendTranslated(message, Integer.toString(number));
|
||||
players.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
||||
}
|
||||
}
|
||||
|
||||
if (leaderboard != null && score > record) {
|
||||
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), score);
|
||||
}
|
||||
|
||||
score = 0;
|
||||
start = null;
|
||||
heading = Option.HEADING.getDirection();
|
||||
|
||||
if (regenerate) { // generate back the blocks
|
||||
player.teleport(playerSpawn);
|
||||
generateFirst(playerSpawn, blockSpawn);
|
||||
return;
|
||||
}
|
||||
|
||||
island.destroy();
|
||||
|
||||
if (getPlayers().isEmpty()) {
|
||||
getSpectators().forEach(spectator -> Modes.DEFAULT.create(spectator.player));
|
||||
}
|
||||
}
|
||||
|
||||
protected void registerScore(String time, String difficulty, int score) {
|
||||
Leaderboard leaderboard = getMode().getLeaderboard();
|
||||
|
||||
if (leaderboard == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getPlayers().forEach(player -> leaderboard.put(player.getUUID(), new Score(player.getName(), time, difficulty, score)));
|
||||
}
|
||||
|
||||
private void deleteSchematic() {
|
||||
if (!deleteSchematic) {
|
||||
return;
|
||||
}
|
||||
LoParkour.log("Deleting schematic");
|
||||
|
||||
schematicBlocks.forEach(block -> block.setType(Material.AIR));
|
||||
schematicBlocks.clear();
|
||||
|
||||
deleteSchematic = false;
|
||||
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next parkour block or schematic.
|
||||
*/
|
||||
public void generate() {
|
||||
if (waitForSchematicCompletion) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<JumpType, Double> chances = new HashMap<>(defaultChances);
|
||||
if (schematicCooldown > 0 || generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS) || profile.get("schematicDifficulty").asDouble() == 0.0 || !schematicBlocks.isEmpty()) {
|
||||
chances.remove(JumpType.SCHEMATIC);
|
||||
}
|
||||
if (!profile.get("useSpecialBlocks").asBoolean()) {
|
||||
chances.remove(JumpType.SPECIAL);
|
||||
}
|
||||
if (chances.isEmpty()) {
|
||||
chances.put(JumpType.DEFAULT, 1.0);
|
||||
}
|
||||
|
||||
JumpType jump = Probs.random(chances);
|
||||
if (jump == JumpType.SCHEMATIC) {
|
||||
LoParkour.log("Generating schematic jump at index %s".formatted(history.size() + 1));
|
||||
|
||||
double difficulty = profile.get("schematicDifficulty").asDouble();
|
||||
|
||||
Schematic schematic = Schematics.getSchematic(LoParkour.getPlugin(),
|
||||
Colls.random(Schematics.getSchematicNames(LoParkour.getPlugin()).stream()
|
||||
.filter(name -> name.contains("parkour-") && getDifficulty(name) <= difficulty)
|
||||
.toList()));
|
||||
|
||||
schematicBlocks = rotatedPaste(schematic, selectBlocks().get(0).getLocation());
|
||||
|
||||
particles(schematicBlocks);
|
||||
sound(schematicBlocks);
|
||||
|
||||
new ParkourSchematicGenerateEvent(schematic, this, player).call();
|
||||
|
||||
if (schematicBlocks.isEmpty()) {
|
||||
LoParkour.logging().stack("Error while trying to paste schematic %s".formatted(schematic.getFile().getName()), new NoSuchElementException("No schematic blocks found"));
|
||||
return;
|
||||
}
|
||||
|
||||
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
waitForSchematicCompletion = true;
|
||||
return;
|
||||
}
|
||||
|
||||
LoParkour.log("Generating normal jump at index %s".formatted(history.size() + 1));
|
||||
|
||||
List<Block> blocks = selectBlocks();
|
||||
|
||||
if (blocks.isEmpty()) {
|
||||
LoParkour.logging().stack("Error while trying to generate parkour", new NoSuchElementException("No blocks to generate found"));
|
||||
return;
|
||||
}
|
||||
|
||||
List<Block> movedBlocks = new ArrayList<>();
|
||||
for (Block block : blocks) {
|
||||
BlockData data = (jump == JumpType.SPECIAL && !generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) ? Probs.random(specialChances) : selectBlockData();
|
||||
|
||||
if (data instanceof Fence) {
|
||||
block = block.getLocation().subtract(0, 1, 0).getBlock();
|
||||
}
|
||||
|
||||
block.setBlockData(data, data instanceof Fence || data instanceof GlassPane);
|
||||
movedBlocks.add(block);
|
||||
}
|
||||
|
||||
new ParkourBlockGenerateEvent(movedBlocks, this, player).call();
|
||||
|
||||
particles(movedBlocks);
|
||||
sound(movedBlocks);
|
||||
|
||||
history.addAll(movedBlocks);
|
||||
schematicCooldown--;
|
||||
}
|
||||
|
||||
private @NotNull List<Block> rotatedPaste(Schematic schematic, Location location) {
|
||||
if (schematic == null || location == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Optional<Vector> optionalStart = schematic.getVectorBlockMap().entrySet().stream()
|
||||
.filter(entry -> {
|
||||
if (entry == null || entry.getValue() == null) return false;
|
||||
|
||||
return entry.getValue().getMaterial() == Material.LIME_WOOL;
|
||||
})
|
||||
.map(Map.Entry::getKey)
|
||||
.findAny();
|
||||
|
||||
Optional<Vector> optionalEnd = schematic.getVectorBlockMap().entrySet().stream()
|
||||
.filter(entry -> {
|
||||
if (entry == null || entry.getValue() == null) return false;
|
||||
|
||||
return entry.getValue().getMaterial() == Material.RED_WOOL;
|
||||
})
|
||||
.map(Map.Entry::getKey)
|
||||
.findAny();
|
||||
|
||||
if (optionalStart.isEmpty()) {
|
||||
LoParkour.logging().stack("Error while trying to find start of schematic", "check if you placed a lime wool block");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (optionalEnd.isEmpty()) {
|
||||
LoParkour.logging().stack("Error while trying to find end of schematic", "check if you placed a red wool block");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Vector start = optionalStart.get();
|
||||
Vector end = optionalEnd.get();
|
||||
Vector startToEnd = end.clone().subtract(start);
|
||||
|
||||
/// snapped vector, supports no rotation if x == z
|
||||
Vector snapped;
|
||||
if (startToEnd.equals(heading)) {
|
||||
snapped = new Vector(0, 0, 0);
|
||||
} else if (Math.abs(startToEnd.getX()) > Math.abs(startToEnd.getZ())) {
|
||||
snapped = new Vector(Math.signum(startToEnd.getX()), 0, 0);
|
||||
} else {
|
||||
snapped = new Vector(0, 0, Math.signum(startToEnd.getZ()));
|
||||
}
|
||||
|
||||
// the angle between heading and normalized direction of schematic, snapped to 90 deg angles
|
||||
double snappedAngle = angleInY(heading, snapped);
|
||||
|
||||
Location rotatedStart = location.clone().subtract(start.clone().rotateAroundY(snappedAngle));
|
||||
Vector rotatedStartToEnd = startToEnd.clone().rotateAroundY(snappedAngle);
|
||||
|
||||
history.add(location.clone().add(rotatedStartToEnd).subtract(0, 1, 0).getBlock());
|
||||
return schematic.paste(rotatedStart, snappedAngle); // only yaw
|
||||
}
|
||||
|
||||
private double angleInY(Vector a, Vector b) {
|
||||
double det = a.getX() * b.getZ() - a.getZ() * b.getX();
|
||||
return Math.atan2(det, a.dot(b));
|
||||
}
|
||||
|
||||
protected Block getLatest() {
|
||||
return history.get(history.size() - 1);
|
||||
}
|
||||
|
||||
private double getDifficulty(String fileName) {
|
||||
String path = "difficulty.%s".formatted(fileName.split("[-.]")[1]);
|
||||
|
||||
if (!Config.SCHEMATICS.isPath(path)) {
|
||||
return 1.0; // todo remove
|
||||
}
|
||||
|
||||
return Config.SCHEMATICS.getDouble(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a specific amount of blocks ahead of the player
|
||||
*
|
||||
* @param amount The amount
|
||||
*/
|
||||
public void generate(int amount) {
|
||||
for (int i = 0; i < amount + 1; i++) {
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the first few blocks (which come off the spawn island)
|
||||
*
|
||||
* @param spawn The spawn of the player
|
||||
* @param block The location used to begin the parkour of off
|
||||
*/
|
||||
public void generateFirst(Location spawn, Location block) {
|
||||
LoParkour.log("First generation");
|
||||
|
||||
playerSpawn = spawn;
|
||||
lastStandingPlayerLocation = spawn;
|
||||
blockSpawn = block;
|
||||
history.add(blockSpawn.getBlock());
|
||||
|
||||
generate(profile.get("blockLead").asInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a score between 0 (inclusive) and 1 (inclusive) to determine how difficult it was for
|
||||
* the player to achieve this score using their settings.
|
||||
*/
|
||||
public double getDifficultyScore() {
|
||||
double score = 0;
|
||||
|
||||
if (profile.get("useSpecialBlocks").asBoolean()) score += 0.5;
|
||||
if (profile.get("schematicDifficulty").asDouble() > 0) {
|
||||
if (profile.get("schematicDifficulty").asDouble() <= 0.25) score += 0.2;
|
||||
else if (profile.get("schematicDifficulty").asDouble() <= 0.5) score += 0.3;
|
||||
else if (profile.get("schematicDifficulty").asDouble() <= 0.75) score += 0.4;
|
||||
else if (profile.get("schematicDifficulty").asDouble() <= 1.0) score += 0.5;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The time in custom format.
|
||||
*/
|
||||
public String getFormattedTime() {
|
||||
return getTime(Config.CONFIG.getString("options.time.score-format"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current detailed duration of the run.
|
||||
*/
|
||||
public String getDetailedTime() {
|
||||
return getTime("mm:ss:SSS");
|
||||
}
|
||||
|
||||
private String getTime(String format) {
|
||||
var timeMs = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
|
||||
|
||||
try {
|
||||
return DateTimeFormatter.ofPattern(format)
|
||||
.withZone(ZoneOffset.UTC)
|
||||
.format(timeMs);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
LoParkour.logging().stack("Invalid score time format %s".formatted(Config.CONFIG.getString("options.time.score-format")), ex);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return This generator's mode.
|
||||
*/
|
||||
public Mode getMode() {
|
||||
return Modes.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The players in this session.
|
||||
*/
|
||||
@NotNull
|
||||
public List<ParkourPlayer> getPlayers() {
|
||||
return session.getPlayers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The spectators in this session.
|
||||
*/
|
||||
@NotNull
|
||||
public List<ParkourSpectator> getSpectators() {
|
||||
return session.getSpectators();
|
||||
}
|
||||
|
||||
public enum JumpType {
|
||||
DEFAULT, SCHEMATIC, SPECIAL
|
||||
}
|
||||
}
|
||||
61
src/main/java/dev/loki/loparkour/generator/Profile.java
Normal file
61
src/main/java/dev/loki/loparkour/generator/Profile.java
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Wrapper class for setting storing.
|
||||
* Allows different modes to change settings without them being saved as being player-selected.
|
||||
*/
|
||||
public class Profile {
|
||||
|
||||
/**
|
||||
* Map of the setting's name and the value.
|
||||
*/
|
||||
public final Map<String, ProfileValue> settings = new HashMap<>();
|
||||
|
||||
@NotNull
|
||||
public Profile set(@NotNull String setting, @NotNull String value) {
|
||||
settings.put(setting, new ProfileValue(value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ProfileValue get(@NotNull String setting) {
|
||||
ProfileValue value = settings.get(setting);
|
||||
|
||||
return value == null ? new ProfileValue("") : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a setting.
|
||||
*
|
||||
* @param value The string value
|
||||
*/
|
||||
public record ProfileValue(String value) {
|
||||
|
||||
/**
|
||||
* @return true when the string value is "true", else "false".
|
||||
*/
|
||||
public boolean asBoolean() {
|
||||
return value.equals("true"); // save parsing
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The value as a double.
|
||||
*/
|
||||
public double asDouble() {
|
||||
return Double.parseDouble(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The value as an int.
|
||||
*/
|
||||
public int asInt() {
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/main/java/dev/loki/loparkour/hook/FloodgateHook.java
Normal file
17
src/main/java/dev/loki/loparkour/hook/FloodgateHook.java
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.geysermc.floodgate.api.FloodgateApi;
|
||||
|
||||
public class FloodgateHook {
|
||||
|
||||
/**
|
||||
* Whether this player is a bedrock player.
|
||||
*
|
||||
* @param player The player.
|
||||
* @return True if this player is a bedrock player, false if not.
|
||||
*/
|
||||
public static boolean isBedrockPlayer(Player player) {
|
||||
return FloodgateApi.getInstance().isFloodgatePlayer(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
84
src/main/java/dev/loki/loparkour/hook/HoloHook.java
Normal file
84
src/main/java/dev/loki/loparkour/hook/HoloHook.java
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import me.filoghost.holographicdisplays.api.HolographicDisplaysAPI;
|
||||
|
||||
public class HoloHook {
|
||||
|
||||
/**
|
||||
* Initializes this hook.
|
||||
*/
|
||||
public static void init() {
|
||||
try {
|
||||
Class.forName("me.filoghost.holographicdisplays.api.HolographicDisplaysAPI");
|
||||
} catch (Exception ex) {
|
||||
LoParkour.logging().warn("##");
|
||||
LoParkour.logging().warn("## LoParkour only supports Holographic Displays v3.0.0 or higher!");
|
||||
LoParkour.logging().warn("## This hook will now be disabled.");
|
||||
LoParkour.logging().warn("##");
|
||||
return;
|
||||
}
|
||||
|
||||
HolographicDisplaysAPI.get(LoParkour.getPlugin()).registerGlobalPlaceholder("ip_leaderboard", 100, argument -> {
|
||||
|
||||
if (argument == null) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
// {ip_leaderboard: default, score, #1}
|
||||
String[] split = argument.replace(" ", "").split(",");
|
||||
|
||||
Mode mode = Registry.getMode(split[0].toLowerCase());
|
||||
|
||||
if (mode == null) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
Leaderboard leaderboard = mode.getLeaderboard();
|
||||
|
||||
if (leaderboard == null) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
String type = split[1].toLowerCase();
|
||||
String rank = split[2].replace("#", "");
|
||||
|
||||
Score score = leaderboard.getScoreAtRank(Integer.parseInt(rank));
|
||||
|
||||
if (score == null) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
return switch (type) {
|
||||
case "score" -> Integer.toString(score.score());
|
||||
case "name" -> score.name();
|
||||
case "time" -> score.time();
|
||||
case "difficulty" -> score.difficulty();
|
||||
case "difficulty_string" -> parseDifficulty(score.difficulty());
|
||||
default -> "?";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static String parseDifficulty(String string) {
|
||||
if (string.contains("?")) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
double difficulty = Double.parseDouble(string);
|
||||
if (difficulty <= 0.25) {
|
||||
return "easy";
|
||||
} else if (difficulty <= 0.5) {
|
||||
return "medium";
|
||||
} else if (difficulty <= 0.75) {
|
||||
return "hard";
|
||||
} else if (difficulty <= 1) {
|
||||
return "very hard";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
200
src/main/java/dev/loki/loparkour/hook/PAPIHook.java
Normal file
200
src/main/java/dev/loki/loparkour/hook/PAPIHook.java
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PAPIHook extends PlaceholderExpansion {
|
||||
|
||||
private static final Pattern INFINITE_REGEX = Pattern.compile("(.+)_(\\d+)");
|
||||
|
||||
@Override
|
||||
public @NotNull String getIdentifier() {
|
||||
return "witp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getAuthor() {
|
||||
return "loki";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRegister() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getVersion() {
|
||||
return LoParkour.getPlugin().getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onPlaceholderRequest(Player player, @NotNull String params) {
|
||||
// placeholders that don't require a player
|
||||
switch (params) {
|
||||
case "version", "ver" -> {
|
||||
return LoParkour.getPlugin().getDescription().getVersion();
|
||||
}
|
||||
case "leader", "record_player" -> {
|
||||
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
|
||||
return score != null ? score.name() : "?";
|
||||
}
|
||||
case "leader_score", "record_score", "record" -> {
|
||||
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
|
||||
return score != null ? Integer.toString(score.score()) : "?";
|
||||
}
|
||||
}
|
||||
|
||||
if (params.contains("player_rank_")) {
|
||||
return getInfiniteScore(params.replace("player_rank_", ""), Score::name);
|
||||
} else if (params.contains("score_rank_")) {
|
||||
return getInfiniteScore(params.replace("score_rank_", ""), Score::score);
|
||||
} else if (params.contains("time_rank_")) {
|
||||
return getInfiniteScore(params.replace("time_rank_", ""), Score::time);
|
||||
} else if (params.contains("difficulty_rank_")) {
|
||||
return getInfiniteScore(params.replace("difficulty_rank_", ""), Score::difficulty);
|
||||
} else if (params.contains("difficulty_string_rank_")) {
|
||||
return getInfiniteScore(params.replace("difficulty_string_rank_", ""),
|
||||
score -> parseDifficulty(Double.parseDouble(score.difficulty().contains("?") ? "2" : score.difficulty())));
|
||||
}
|
||||
|
||||
// placeholders that require player
|
||||
if (player == null) {
|
||||
return "player doesn't exist";
|
||||
}
|
||||
|
||||
switch (params) {
|
||||
case "rank" -> {
|
||||
return Integer.toString(Modes.DEFAULT.getLeaderboard().getRank(player.getUniqueId()));
|
||||
}
|
||||
case "highscore", "high_score" -> {
|
||||
return Integer.toString(Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).score());
|
||||
}
|
||||
case "high_score_time" -> {
|
||||
return Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).time();
|
||||
}
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
ParkourPlayer pp = null;
|
||||
if (user instanceof ParkourPlayer) {
|
||||
pp = (ParkourPlayer) user;
|
||||
} else if (user instanceof ParkourSpectator) {
|
||||
pp = ((ParkourSpectator) user).closest;
|
||||
}
|
||||
|
||||
if (pp != null && pp.session.generator != null) {
|
||||
ParkourGenerator generator = pp.session.generator;
|
||||
switch (params) {
|
||||
case "score", "current_score" -> {
|
||||
return Integer.toString(generator.score);
|
||||
}
|
||||
case "time", "current_time" -> {
|
||||
return generator.getFormattedTime();
|
||||
}
|
||||
case "blocklead", "lead" -> {
|
||||
return Integer.toString(pp.blockLead);
|
||||
}
|
||||
case "style" -> {
|
||||
return pp.style;
|
||||
}
|
||||
case "time_pref", "time_preference" -> {
|
||||
return Integer.toString(pp.selectedTime);
|
||||
}
|
||||
case "scoreboard" -> {
|
||||
return pp.showScoreboard.toString();
|
||||
}
|
||||
case "difficulty" -> {
|
||||
return Double.toString(pp.schematicDifficulty);
|
||||
}
|
||||
case "difficulty_string" -> {
|
||||
return parseDifficulty(pp.schematicDifficulty);
|
||||
}
|
||||
default -> {
|
||||
if (params.contains("score_until_")) {
|
||||
String replaced = params.replace("score_until_", "");
|
||||
int interval = Integer.parseInt(replaced);
|
||||
if (interval > 0) {
|
||||
return Integer.toString(interval - (generator.totalScore % interval)); // 100 - (5 % 100) = 95
|
||||
} else {
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String parseDifficulty(double difficulty) {
|
||||
if (difficulty <= 0.25) {
|
||||
return "easy";
|
||||
} else if (difficulty <= 0.5) {
|
||||
return "medium";
|
||||
} else if (difficulty <= 0.75) {
|
||||
return "hard";
|
||||
} else if (difficulty <= 1) {
|
||||
return "very hard";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
|
||||
private String getInfiniteScore(String rankData, Function<Score, ?> f) {
|
||||
int rank;
|
||||
Leaderboard leaderboard = null;
|
||||
Matcher matcher = INFINITE_REGEX.matcher(rankData);
|
||||
|
||||
// use mode-specific format
|
||||
// x_mode_rank
|
||||
if (matcher.matches()) {
|
||||
String name = matcher.group(1);
|
||||
rank = Integer.parseInt(matcher.group(2));
|
||||
|
||||
Mode mode = Registry.getMode(name);
|
||||
if (mode != null) {
|
||||
leaderboard = mode.getLeaderboard();
|
||||
}
|
||||
// use generic format
|
||||
// x_rank
|
||||
} else {
|
||||
rank = Integer.parseInt(rankData);
|
||||
}
|
||||
|
||||
if (leaderboard == null) {
|
||||
leaderboard = Modes.DEFAULT.getLeaderboard();
|
||||
}
|
||||
|
||||
if (rank > 0) {
|
||||
Score score = leaderboard.getScoreAtRank(rank);
|
||||
|
||||
if (score == null) {
|
||||
return "?";
|
||||
} else {
|
||||
return String.valueOf(f.apply(score));
|
||||
}
|
||||
} else {
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/main/java/dev/loki/loparkour/hook/VaultHook.java
Normal file
39
src/main/java/dev/loki/loparkour/hook/VaultHook.java
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public class VaultHook {
|
||||
|
||||
private static Economy economy;
|
||||
|
||||
/**
|
||||
* Deposits amount to the bal of player.
|
||||
*
|
||||
* @param player The player.
|
||||
* @param amount The amount.
|
||||
*/
|
||||
public static void deposit(Player player, double amount) {
|
||||
if (Bukkit.getPluginManager().getPlugin("Vault") == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (economy == null) {
|
||||
RegisteredServiceProvider<Economy> service = Bukkit.getServicesManager().getRegistration(Economy.class);
|
||||
|
||||
if (service == null) {
|
||||
LoParkour.logging().stack("Error while trying to fetch the Vault economy", new NoSuchElementException("No economy found"));
|
||||
return;
|
||||
}
|
||||
|
||||
economy = service.getProvider();
|
||||
}
|
||||
|
||||
economy.depositPlayer(player, amount);
|
||||
}
|
||||
}
|
||||
204
src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java
Normal file
204
src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package dev.loki.loparkour.leaderboard;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class for handling leaderboards.
|
||||
*/
|
||||
public class Leaderboard {
|
||||
|
||||
/**
|
||||
* The mode that this leaderboard belongs to
|
||||
*/
|
||||
public final String mode;
|
||||
|
||||
/**
|
||||
* The way in which items will be sorted.
|
||||
*/
|
||||
public final Sort sort;
|
||||
|
||||
/**
|
||||
* A map of all scores for this mode
|
||||
*/
|
||||
public final Map<UUID, Score> scores = new LinkedHashMap<>();
|
||||
|
||||
public Leaderboard(@NotNull String mode, Sort sort) {
|
||||
this.mode = mode.toLowerCase();
|
||||
this.sort = sort;
|
||||
|
||||
Storage.init(mode);
|
||||
|
||||
// read all data
|
||||
read(true);
|
||||
|
||||
var interval = Config.CONFIG.getInt("storage-update-interval");
|
||||
|
||||
// read/write all data every x seconds after x seconds to allow time for reading/writing
|
||||
Task.create(LoParkour.getPlugin())
|
||||
.delay(interval * 20)
|
||||
.repeat(interval * 20)
|
||||
.async()
|
||||
.execute(Config.CONFIG.getBoolean("joining") ? () -> {
|
||||
LoParkour.log("Periodic saving of leaderboard data of %s".formatted(mode));
|
||||
|
||||
write(true);
|
||||
} : () -> {
|
||||
LoParkour.log("Periodic reading of leaderboard data of %s".formatted(mode));
|
||||
|
||||
read(true);
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all scores to the leaderboard file associated with this leaderboard
|
||||
*/
|
||||
public void write(boolean async) {
|
||||
LoParkour.log("Saving leaderboard data of %s".formatted(mode));
|
||||
|
||||
run(() -> Storage.writeScores(mode, scores), async);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all scores from the leaderboard file
|
||||
*/
|
||||
public void read(boolean async) {
|
||||
LoParkour.log("Reading leaderboard data of %s".formatted(mode));
|
||||
|
||||
run(() -> {
|
||||
scores.clear();
|
||||
scores.putAll(Storage.readScores(mode));
|
||||
|
||||
sort();
|
||||
}, async);
|
||||
}
|
||||
|
||||
private void run(Runnable runnable, boolean async) {
|
||||
if (async) {
|
||||
Task.create(LoParkour.getPlugin()).async().execute(runnable).run();
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sorted copy of the score map.
|
||||
* @param sort The sorting method.
|
||||
* @return A sorted map of scores.
|
||||
*/
|
||||
public Map<UUID, Score> sort(Sort sort) {
|
||||
LinkedHashMap<UUID, Score> sorted = new LinkedHashMap<>();
|
||||
|
||||
scores.entrySet().stream()
|
||||
.sorted((one, two) -> {
|
||||
switch (sort) {
|
||||
case SCORE -> {
|
||||
int scoreComparison = two.getValue().score() - one.getValue().score();
|
||||
|
||||
if (scoreComparison != 0) {
|
||||
return scoreComparison;
|
||||
} else {
|
||||
return one.getValue().getTimeMillis() - two.getValue().getTimeMillis();
|
||||
}
|
||||
}
|
||||
case TIME -> {
|
||||
return one.getValue().getTimeMillis() - two.getValue().getTimeMillis();
|
||||
}
|
||||
case DIFFICULTY -> {
|
||||
return (int) Math.signum(Double.parseDouble(two.getValue().difficulty()) -
|
||||
Double.parseDouble(one.getValue().difficulty()));
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Invalid sort method");
|
||||
}
|
||||
})
|
||||
.forEachOrdered(entry -> sorted.put(entry.getKey(), entry.getValue()));
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// sorts all scores in the map
|
||||
private void sort() {
|
||||
var sorted = sort(sort);
|
||||
|
||||
scores.clear();
|
||||
scores.putAll(sorted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new score, overriding the old one
|
||||
*
|
||||
* @param uuid The player's uuid
|
||||
* @param score The {@link Score} instance associated with a player's run
|
||||
* @return the previous score, if there was one
|
||||
*/
|
||||
@Nullable
|
||||
public Score put(@NotNull UUID uuid, @NotNull Score score) {
|
||||
Score previous = scores.put(uuid, score);
|
||||
|
||||
sort();
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the score of a player by deleting it from the internal map
|
||||
*
|
||||
* @param uuid The UUID
|
||||
* @return the previous value if one was found
|
||||
*/
|
||||
@Nullable
|
||||
public Score remove(@NotNull UUID uuid) {
|
||||
return scores.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all registered scores for this mode
|
||||
*/
|
||||
public void resetAll() {
|
||||
new HashSet<>(scores.keySet()).forEach(this::remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid The {@link UUID} to get.
|
||||
* @return The {@link Score} associated with the player. If null, returns a {@link Score} instance with "?".
|
||||
*/
|
||||
@NotNull
|
||||
public Score get(@NotNull UUID uuid) {
|
||||
return scores.getOrDefault(uuid, new Score("?", "?", "?", 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid The uuid
|
||||
* @return The rank. Starts from 1. Returns 0 if no ranking is found.
|
||||
*/
|
||||
public int getRank(@NotNull UUID uuid) {
|
||||
return new ArrayList<>(scores.keySet()).indexOf(uuid) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the score at a specified rank.
|
||||
* Ranks start at 1.
|
||||
*
|
||||
* @param rank The rank
|
||||
* @return the {@link Score} instance, null if one isn't found
|
||||
*/
|
||||
@Nullable
|
||||
public Score getScoreAtRank(int rank) {
|
||||
if (scores.size() < rank) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ArrayList<>(scores.values()).get(rank - 1);
|
||||
}
|
||||
|
||||
public enum Sort {
|
||||
SCORE, TIME, DIFFICULTY
|
||||
}
|
||||
}
|
||||
42
src/main/java/dev/loki/loparkour/leaderboard/Score.java
Normal file
42
src/main/java/dev/loki/loparkour/leaderboard/Score.java
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.loparkour.leaderboard;
|
||||
|
||||
/**
|
||||
* Represents a record, used to keep track of the score a player may achieve.
|
||||
*
|
||||
* @param name The name of the player
|
||||
* @param score The score achieved
|
||||
* @param time The time it took to achieve this score
|
||||
* @param difficulty The difficulty of this run
|
||||
*/
|
||||
public record Score(String name, String time, String difficulty, int score) {
|
||||
|
||||
/**
|
||||
* Gets a {@link Score} instance from a string
|
||||
*
|
||||
* @param string The string
|
||||
* @return a {@link Score} instance based off the provided string
|
||||
*/
|
||||
public static Score fromString(String string) {
|
||||
String[] parts = string.split(",");
|
||||
|
||||
return new Score(parts[0], parts[1], parts[2], Integer.parseInt(parts[3]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return This score's time in millis.
|
||||
*/
|
||||
public int getTimeMillis() {
|
||||
String[] split = time.split(":");
|
||||
|
||||
int m = Integer.parseInt(split[0]);
|
||||
int s = Integer.parseInt(split[1]);
|
||||
int ms = Integer.parseInt(split[2]);
|
||||
|
||||
return m * 60 * 1000 + s * 1000 + ms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s,%s,%s,%s", name, time, difficulty, score);
|
||||
}
|
||||
}
|
||||
76
src/main/java/dev/loki/loparkour/menu/DynamicMenu.java
Normal file
76
src/main/java/dev/loki/loparkour/menu/DynamicMenu.java
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* A class for menus where items are dynamically displayed; that is, depending on provided conditions.
|
||||
* These conditions are specified by the param {@code shouldDisplay} in the method {@link #registerMainItem(int, int, BiFunction, Predicate)}.
|
||||
*/
|
||||
public abstract class DynamicMenu {
|
||||
|
||||
protected final Map<Integer, List<ItemContainer>> registeredItems = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Registers an item that will be displayed in a specific slot if the specified condition is met.
|
||||
* This will be used to create a context-aware main menu.
|
||||
*
|
||||
* @param row The row in which this item will be displayed. Starts from 0 and ends at 5.
|
||||
* @param id The id of this item. This will be used to determine the positions of each item.
|
||||
* Lower is more to the left. Can't be lower than 0.
|
||||
* @param item The item
|
||||
* @param shouldDisplay Whether this item should be displayed right now.
|
||||
*/
|
||||
public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, MenuItem> item, Predicate<Player> shouldDisplay) {
|
||||
if (id < 0 || row < 0 || row > 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ItemContainer> existing = registeredItems.get(row);
|
||||
if (existing == null) {
|
||||
existing = new ArrayList<>();
|
||||
}
|
||||
existing.add(new ItemContainer(id, item, shouldDisplay));
|
||||
|
||||
registeredItems.put(row, existing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens this dynamic menu to a player
|
||||
*
|
||||
* @param player The player
|
||||
* @param menu The menu
|
||||
*/
|
||||
public void display(Player player, Menu menu) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
for (int row : registeredItems.keySet()) {
|
||||
int actualSlot = row * 9; // 0, 9, 18, etc.
|
||||
|
||||
List<ItemContainer> containers = registeredItems.get(row); // sort by id first
|
||||
containers.sort(Comparator.comparingInt(container -> container.id));
|
||||
|
||||
for (ItemContainer container : containers) {
|
||||
if (container.predicate.test(player)) { // if item in id passes predicate, display it in the menu
|
||||
menu.item(actualSlot, container.item.apply(player, user));
|
||||
actualSlot++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class for registered items
|
||||
*/
|
||||
protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, MenuItem> item,
|
||||
Predicate<Player> predicate) {}
|
||||
}
|
||||
24
src/main/java/dev/loki/loparkour/menu/MainMenu.java
Normal file
24
src/main/java/dev/loki/loparkour/menu/MainMenu.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class MainMenu extends DynamicMenu {
|
||||
|
||||
public MainMenu() {
|
||||
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "play.item").click(event -> Menus.PLAY.open(event.getPlayer())), player -> ParkourOption.PLAY.mayPerform(player) && Config.CONFIG.getBoolean("joining"));
|
||||
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "community.item").click(event -> Menus.COMMUNITY.open(event.getPlayer())), ParkourOption.COMMUNITY::mayPerform);
|
||||
registerMainItem(1, 2, (player, user) -> Locales.getItem(player, "settings.item").click(event -> Menus.SETTINGS.open(event.getPlayer())), player -> ParkourOption.SETTINGS.mayPerform(player) && ParkourUser.isUser(player));
|
||||
registerMainItem(1, 3, (player, user) -> Locales.getItem(player, "lobby.item").click(event -> Menus.LOBBY.open(event.getPlayer())), player -> ParkourOption.LOBBY.mayPerform(player) && ParkourUser.isUser(player));
|
||||
registerMainItem(1, 4, (player, user) -> Locales.getItem(player, "other.quit").click(event -> ParkourPlayer.leave(player)), player -> ParkourOption.QUIT.mayPerform(player) && ParkourUser.isUser(player));
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
display(player, new Menu(3, Locales.getString(player, "main.name"))
|
||||
.distributeRowsEvenly());
|
||||
}
|
||||
}
|
||||
39
src/main/java/dev/loki/loparkour/menu/Menus.java
Normal file
39
src/main/java/dev/loki/loparkour/menu/Menus.java
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
|
||||
import dev.loki.loparkour.menu.community.CommunityMenu;
|
||||
import dev.loki.loparkour.menu.community.LeaderboardsMenu;
|
||||
import dev.loki.loparkour.menu.community.SingleLeaderboardMenu;
|
||||
import dev.loki.loparkour.menu.lobby.LobbyMenu;
|
||||
import dev.loki.loparkour.menu.lobby.PlayerManagementMenu;
|
||||
import dev.loki.loparkour.menu.play.PlayMenu;
|
||||
import dev.loki.loparkour.menu.play.SingleMenu;
|
||||
import dev.loki.loparkour.menu.play.SpectatorMenu;
|
||||
import dev.loki.loparkour.menu.settings.LangMenu;
|
||||
import dev.loki.loparkour.menu.settings.ParkourSettingsMenu;
|
||||
import dev.loki.loparkour.menu.settings.SettingsMenu;
|
||||
|
||||
public class Menus {
|
||||
|
||||
// main
|
||||
public static MainMenu MAIN = new MainMenu();
|
||||
|
||||
// play
|
||||
public static PlayMenu PLAY = new PlayMenu();
|
||||
public static SingleMenu SINGLE = new SingleMenu();
|
||||
public static SpectatorMenu SPECTATOR = new SpectatorMenu();
|
||||
|
||||
// community
|
||||
public static CommunityMenu COMMUNITY = new CommunityMenu();
|
||||
public static LeaderboardsMenu LEADERBOARDS = new LeaderboardsMenu();
|
||||
public static SingleLeaderboardMenu SINGLE_LEADERBOARD = new SingleLeaderboardMenu();
|
||||
|
||||
// settings
|
||||
public static SettingsMenu SETTINGS = new SettingsMenu();
|
||||
public static LangMenu LANG = new LangMenu();
|
||||
public static ParkourSettingsMenu PARKOUR_SETTINGS = new ParkourSettingsMenu();
|
||||
|
||||
// lobby
|
||||
public static LobbyMenu LOBBY = new LobbyMenu();
|
||||
public static PlayerManagementMenu PLAYER_MANAGEMENT = new PlayerManagementMenu();
|
||||
|
||||
}
|
||||
81
src/main/java/dev/loki/loparkour/menu/ParkourOption.java
Normal file
81
src/main/java/dev/loki/loparkour/menu/ParkourOption.java
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import org.bukkit.permissions.Permissible;
|
||||
|
||||
/**
|
||||
* An enum for all Parkour Menu Options
|
||||
*/
|
||||
public enum ParkourOption {
|
||||
|
||||
// main
|
||||
MAIN("main", "LoParkour.main"),
|
||||
|
||||
// play
|
||||
PLAY("play", "LoParkour.play"),
|
||||
SINGLE("play.single", "LoParkour.play.single"),
|
||||
SPECTATOR("play.spectator", "LoParkour.play.spectator"),
|
||||
|
||||
// community
|
||||
COMMUNITY("community", "LoParkour.community"),
|
||||
LEADERBOARDS("community.leaderboards", "LoParkour.community.leaderboards"),
|
||||
|
||||
// settings
|
||||
SETTINGS("settings", "LoParkour.settings"),
|
||||
|
||||
PARKOUR_SETTINGS("settings.parkour_settings.item", "LoParkour.settings.parkour_settings"),
|
||||
STYLES("settings.parkour_settings.items.styles", "LoParkour.settings.styles"),
|
||||
LEADS("settings.parkour_settings.items.leads", "LoParkour.settings.leads"),
|
||||
TIME("settings.parkour_settings.items.time", "LoParkour.settings.time"),
|
||||
SCHEMATICS("settings.parkour_settings.items.schematics", "LoParkour.settings.schematics"),
|
||||
SCOREBOARD("settings.parkour_settings.items.scoreboard", "LoParkour.settings.show_scoreboard"),
|
||||
FALL_MESSAGE("settings.parkour_settings.items.fall_message", "LoParkour.settings.fall_message"),
|
||||
PARTICLES("settings.parkour_settings.items.particles", "LoParkour.settings.particles"),
|
||||
SOUND("settings.parkour_settings.items.sound", "LoParkour.settings.sound"),
|
||||
SPECIAL_BLOCKS("settings.parkour_settings.items.special_blocks", "LoParkour.settings.special_blocks"),
|
||||
|
||||
LANG("settings.lang", "LoParkour.settings.lang"),
|
||||
CHAT("settings.chat", "LoParkour.settings.chat"),
|
||||
|
||||
// lobby
|
||||
LOBBY("lobby", "LoParkour.lobby"),
|
||||
VISIBILITY("lobby.visibility", "LoParkour.lobby.visibility"),
|
||||
PLAYER_MANAGEMENT("lobby.player_management", "LoParkour.lobby.player_management"),
|
||||
|
||||
// other
|
||||
JOIN("join", "LoParkour.join"),
|
||||
QUIT("quit", "LoParkour.quit"),
|
||||
ADMIN("admin", "LoParkour.admin");
|
||||
|
||||
/**
|
||||
* The path in config files for this option.
|
||||
*/
|
||||
public final String path;
|
||||
|
||||
/**
|
||||
* The permission for this option.
|
||||
*/
|
||||
public final String permission;
|
||||
|
||||
ParkourOption(String path, String permission) {
|
||||
this.path = path;
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param permissible The player
|
||||
* @return True if the player is allowed to view/perform this option, false if not.
|
||||
*/
|
||||
public boolean mayPerform(Permissible permissible) {
|
||||
boolean value = Option.OPTIONS_ENABLED.getOrDefault(this, true);
|
||||
|
||||
if (value) {
|
||||
if (Config.CONFIG.getBoolean("permissions.enabled")) {
|
||||
return permissible.hasPermission(permission);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package dev.loki.loparkour.menu.community;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.DynamicMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* The menu for all community-related things
|
||||
*/
|
||||
public class CommunityMenu extends DynamicMenu {
|
||||
|
||||
public CommunityMenu() {
|
||||
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "community.leaderboards.item").click(event -> Menus.LEADERBOARDS.open(event.getPlayer())), ParkourOption.LEADERBOARDS::mayPerform);
|
||||
registerMainItem(2, 10, (player, user) -> Locales.getItem(player, "other.close").click(event -> event.getPlayer().closeInventory()), player -> true);
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
display(player, new Menu(3, Locales.getString(player, "community.name"))
|
||||
.distributeRowsEvenly());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package dev.loki.loparkour.menu.community;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.inventory.PagedMenu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Leaderboards menu
|
||||
*/
|
||||
public class LeaderboardsMenu {
|
||||
|
||||
public void open(Player player) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
|
||||
PagedMenu menu = new PagedMenu(3, Locales.getString(player, "%s.name".formatted(ParkourOption.LEADERBOARDS.path)));
|
||||
|
||||
Mode latest = null;
|
||||
List<MenuItem> items = new ArrayList<>();
|
||||
for (Mode mode : Registry.getModes()) {
|
||||
Leaderboard leaderboard = mode.getLeaderboard();
|
||||
Item item = mode.getItem(locale);
|
||||
|
||||
if (leaderboard == null || item == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.add(item.clone().click(event -> Menus.SINGLE_LEADERBOARD.open(player, mode, leaderboard.sort)));
|
||||
latest = mode;
|
||||
}
|
||||
|
||||
if (items.size() == 1) {
|
||||
Menus.SINGLE_LEADERBOARD.open(player, latest, Leaderboard.Sort.SCORE);
|
||||
return;
|
||||
}
|
||||
|
||||
menu.displayRows(0, 1)
|
||||
.addToDisplay(items)
|
||||
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
|
||||
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
|
||||
.item(22, Locales.getItem(player, "other.close").click(event -> Menus.COMMUNITY.open(event.getPlayer())))
|
||||
.open(player);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package dev.loki.loparkour.menu.community;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.inventory.PagedMenu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import dev.efnilite.vilib.util.SkullSetter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Menu for a single leaderboard
|
||||
*/
|
||||
public class SingleLeaderboardMenu {
|
||||
|
||||
public void open(Player player, Mode mode, Leaderboard.Sort sort) {
|
||||
Leaderboard leaderboard = mode.getLeaderboard();
|
||||
|
||||
if (leaderboard == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// init vars
|
||||
var user = ParkourUser.getUser(player);
|
||||
var locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
var menu = new PagedMenu(3, Locales.getString(player, "%s.name".formatted(ParkourOption.LEADERBOARDS.path)));
|
||||
|
||||
var items = new ArrayList<MenuItem>();
|
||||
|
||||
var base = Locales.getItem(player, "%s.head".formatted(ParkourOption.LEADERBOARDS.path));
|
||||
|
||||
for (Map.Entry<UUID, Score> entry : leaderboard.sort(sort).entrySet()) {
|
||||
int rank = items.size() + 1;
|
||||
|
||||
var uuid = entry.getKey();
|
||||
var score = entry.getValue();
|
||||
|
||||
if (score == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Item item = base.clone().material(Material.PLAYER_HEAD)
|
||||
.modifyName(name -> name.replace("%r", Integer.toString(rank))
|
||||
.replace("%s", Integer.toString(score.score()))
|
||||
.replace("%p", score.name())
|
||||
.replace("%t", score.time())
|
||||
.replace("%d", score.difficulty()))
|
||||
.modifyLore(line -> line.replace("%r", Integer.toString(rank))
|
||||
.replace("%s", Integer.toString(score.score()))
|
||||
.replace("%p", score.name())
|
||||
.replace("%t", score.time())
|
||||
.replace("%d", score.difficulty()));
|
||||
|
||||
// Player head gathering
|
||||
ItemStack stack = item.build();
|
||||
|
||||
// if there are more than 36 players, don't show the heads to avoid server crashing
|
||||
// and bedrock has no player skull support
|
||||
if (rank <= 20 && !ParkourUser.isBedrockPlayer(player)) {
|
||||
OfflinePlayer op = Bukkit.getOfflinePlayer(uuid);
|
||||
|
||||
if (op.getName() != null && !op.getName().startsWith(".")) { // bedrock players' names with geyser start with a .
|
||||
SkullMeta meta = (SkullMeta) stack.getItemMeta();
|
||||
|
||||
if (meta != null) {
|
||||
SkullSetter.setPlayerHead(op, meta);
|
||||
item.meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uuid.equals(player.getUniqueId())) {
|
||||
menu.item(21, item.clone());
|
||||
}
|
||||
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
List<String> values = Locales.getStringList(locale, "%s.sort.values".formatted(ParkourOption.LEADERBOARDS.path));
|
||||
|
||||
if (values.size() != 3) {
|
||||
LoParkour.logging().stack("Error while trying to get locales for sort values: not enough sort values present",
|
||||
"check your %s locale file".formatted(locale), new IllegalArgumentException());
|
||||
}
|
||||
|
||||
String name = switch (sort) {
|
||||
case SCORE -> values.get(0);
|
||||
case TIME -> values.get(1);
|
||||
case DIFFICULTY -> values.get(2);
|
||||
};
|
||||
|
||||
// get next sorting type
|
||||
var next = switch (sort) {
|
||||
case SCORE -> Leaderboard.Sort.TIME;
|
||||
case TIME -> Leaderboard.Sort.DIFFICULTY;
|
||||
default -> Leaderboard.Sort.SCORE;
|
||||
};
|
||||
|
||||
menu.displayRows(0, 1)
|
||||
.addToDisplay(items)
|
||||
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
|
||||
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
|
||||
.item(22, Locales.getItem(player, ParkourOption.LEADERBOARDS.path + ".sort", name.toLowerCase()).click(event -> open(player, mode, next)))
|
||||
.item(23, Locales.getItem(player, "other.close").click(event -> {
|
||||
List<Mode> modes = Registry.getModes()
|
||||
.stream()
|
||||
.filter(m -> m.getLeaderboard() != null && m.getItem(locale) != null)
|
||||
.toList();
|
||||
|
||||
if (modes.size() == 1) {
|
||||
Menus.COMMUNITY.open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
Menus.LEADERBOARDS.open(event.getPlayer());
|
||||
}))
|
||||
.open(player);
|
||||
}
|
||||
}
|
||||
79
src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java
Normal file
79
src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package dev.loki.loparkour.menu.lobby;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.DynamicMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.item.SliderItem;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LobbyMenu extends DynamicMenu {
|
||||
|
||||
public LobbyMenu() {
|
||||
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "lobby.player_management.item").click(event -> Menus.PLAYER_MANAGEMENT.open(player)), player -> {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
|
||||
return ParkourOption.PLAYER_MANAGEMENT.mayPerform(player) && user instanceof ParkourPlayer && user.session.getPlayers().get(0) == user;
|
||||
});
|
||||
|
||||
registerMainItem(1, 1, (player, user) -> {
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> values = Locales.getStringList(user.locale, "lobby.visibility.values");
|
||||
|
||||
return new SliderItem().initial(switch (user.session.getVisibility()) {
|
||||
case PUBLIC -> 0;
|
||||
case ID_ONLY -> 1;
|
||||
case PRIVATE -> 2;
|
||||
}).add(0, Locales.getItem(player, "lobby.visibility").modifyLore(lore -> lore.replace("%s", values.get(2))), event -> { // public
|
||||
ParkourUser u = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (u != null) {
|
||||
u.session.setVisibility(Session.Visibility.PUBLIC);
|
||||
}
|
||||
|
||||
return true;
|
||||
}).add(1, Locales.getItem(player, "lobby.visibility").modifyLore(lore -> lore.replace("%s", values.get(1))), event -> { // id only
|
||||
ParkourUser u = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (u != null) {
|
||||
u.session.setVisibility(Session.Visibility.ID_ONLY);
|
||||
}
|
||||
|
||||
return true;
|
||||
}).add(2, Locales.getItem(player, "lobby.visibility").modifyLore(lore -> lore.replace("%s", values.get(0))), event -> { // private
|
||||
ParkourUser u = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (u != null) {
|
||||
u.session.setVisibility(Session.Visibility.PRIVATE);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, player -> {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
|
||||
return ParkourOption.VISIBILITY.mayPerform(player) && user instanceof ParkourPlayer && user.session.getPlayers().get(0) == user;
|
||||
});
|
||||
|
||||
registerMainItem(2, 10, (player, user) -> Locales.getItem(player, "other.close").click(event -> event.getPlayer().closeInventory()), player -> true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the main menu.
|
||||
*
|
||||
* @param player The player to open the menu to
|
||||
*/
|
||||
public void open(Player player) {
|
||||
display(player, new Menu(3, Locales.getString(player, "lobby.name"))
|
||||
.distributeRowsEvenly());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package dev.loki.loparkour.menu.lobby;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.PagedMenu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.util.SkullSetter;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Menu for managing players
|
||||
*/
|
||||
public class PlayerManagementMenu {
|
||||
|
||||
public void open(Player p) {
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer viewer = ParkourPlayer.getPlayer(p);
|
||||
|
||||
if (viewer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Session session = viewer.session;
|
||||
|
||||
PagedMenu menu = new PagedMenu(3, Locales.getString(viewer.locale, "lobby.player_management.name"));
|
||||
add(menu, viewer, session.getPlayers().stream().map(player -> (ParkourUser) player).toList());
|
||||
add(menu, viewer, session.getSpectators().stream().map(player -> (ParkourUser) player).toList());
|
||||
|
||||
menu.displayRows(0, 1)
|
||||
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
|
||||
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
|
||||
.item(22, Locales.getItem(viewer.locale, "other.close").click(event -> Menus.LOBBY.open(event.getPlayer())))
|
||||
.open(p);
|
||||
}
|
||||
|
||||
private void add(PagedMenu menu, ParkourUser viewer, Collection<ParkourUser> users) {
|
||||
Session session = viewer.session;
|
||||
|
||||
for (ParkourUser other : users) {
|
||||
if (other == viewer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Item item = Locales.getItem(viewer.locale, "lobby.player_management.head", other.getName());
|
||||
item.material(Material.PLAYER_HEAD);
|
||||
|
||||
boolean muted = session.isMuted(other);
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
if (muted) {
|
||||
// add top
|
||||
lore.addAll(List.of(Locales.getString(viewer.locale, "lobby.player_management.head.top").split("\\|\\|")));
|
||||
}
|
||||
|
||||
// add bottom
|
||||
lore.addAll(List.of(Locales.getString(viewer.locale, "lobby.player_management.head.bottom").split("\\|\\|")));
|
||||
|
||||
// Player head gathering
|
||||
item.material(Material.PLAYER_HEAD).lore(lore).click(event -> {
|
||||
ClickType click = event.event().getClick();
|
||||
|
||||
switch (click) {
|
||||
case LEFT -> {
|
||||
Modes.DEFAULT.create(other.player);
|
||||
|
||||
other.sendTranslated("lobby.player_management.kicked");
|
||||
|
||||
viewer.sendTranslated("lobby.player_management.advice");
|
||||
open(viewer.player);
|
||||
}
|
||||
case RIGHT -> {
|
||||
session.toggleMute(other);
|
||||
|
||||
if (!muted) {
|
||||
other.sendTranslated("lobby.player_management.muted");
|
||||
} else {
|
||||
other.sendTranslated("lobby.player_management.unmuted");
|
||||
}
|
||||
|
||||
open(viewer.player);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ItemStack stack = item.build(); // Updating meta requires building
|
||||
stack.setType(Material.PLAYER_HEAD);
|
||||
|
||||
// bedrock has no player skull support
|
||||
if (menu.getTotalToDisplay().size() <= 36 && !ParkourUser.isBedrockPlayer(other.player)) {
|
||||
if (other.getName() != null && !other.getName().startsWith(".")) { // bedrock players' names with geyser start with a .
|
||||
SkullMeta meta = (SkullMeta) stack.getItemMeta();
|
||||
|
||||
if (meta != null) {
|
||||
SkullSetter.setPlayerHead(other.player, meta);
|
||||
item.meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu.addToDisplay(List.of(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/main/java/dev/loki/loparkour/menu/play/PlayMenu.java
Normal file
38
src/main/java/dev/loki/loparkour/menu/play/PlayMenu.java
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package dev.loki.loparkour.menu.play;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.DynamicMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* The menu where players can join modes
|
||||
*/
|
||||
public class PlayMenu extends DynamicMenu {
|
||||
|
||||
public PlayMenu() {
|
||||
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "play.single.item")
|
||||
.click(event -> Menus.SINGLE.open(event.getPlayer())),
|
||||
ParkourOption.SINGLE::mayPerform);
|
||||
|
||||
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "other.iep")
|
||||
.click(event -> event.getPlayer().performCommand("iep play")),
|
||||
player -> Bukkit.getPluginManager().isPluginEnabled("IEP"));
|
||||
|
||||
registerMainItem(1, 6, (player, user) -> Locales.getItem(player, "play.spectator.item")
|
||||
.click(event -> Menus.SPECTATOR.open(event.getPlayer())),
|
||||
ParkourOption.SPECTATOR::mayPerform);
|
||||
|
||||
registerMainItem(2, 0, (player, user) -> Locales.getItem(player, "other.close")
|
||||
.click(event -> event.getPlayer().closeInventory()),
|
||||
player -> true);
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
display(player, new Menu(3, Locales.getString(player, "play.name"))
|
||||
.distributeRowsEvenly());
|
||||
}
|
||||
}
|
||||
68
src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java
Normal file
68
src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package dev.loki.loparkour.menu.play;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.MultiMode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.efnilite.vilib.inventory.PagedMenu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The menu to select single player modes
|
||||
*/
|
||||
public class SingleMenu {
|
||||
|
||||
public void open(Player player) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
|
||||
List<Mode> modes = Registry.getModes();
|
||||
|
||||
List<MenuItem> items = new ArrayList<>();
|
||||
List<Mode> modeSet = new ArrayList<>();
|
||||
for (Mode mode : modes) {
|
||||
boolean permissions = Config.CONFIG.getBoolean("permissions.enabled") && !player.hasPermission("LoParkour.gamemode." + mode.getName());
|
||||
|
||||
Item item = mode.getItem(locale);
|
||||
|
||||
if (permissions || mode instanceof MultiMode || item == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modeSet.add(mode);
|
||||
|
||||
items.add(item.clone().click(event -> {
|
||||
if (user == null || Duration.between(user.joined, Instant.now()).toSeconds() > 3) {
|
||||
mode.create(player);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (modeSet.size() == 1) {
|
||||
modeSet.get(0).create(player);
|
||||
return;
|
||||
}
|
||||
|
||||
PagedMenu mode = new PagedMenu(3, Locales.getString(player, "play.single.name"));
|
||||
mode.displayRows(0, 1)
|
||||
.addToDisplay(items)
|
||||
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> mode.page(1)))
|
||||
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> mode.page(-1)))
|
||||
.item(22, Locales.getItem(player, "other.close").click(event -> Menus.PLAY.open(event.getPlayer())))
|
||||
.open(player);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package dev.loki.loparkour.menu.play;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import dev.efnilite.vilib.inventory.PagedMenu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import dev.efnilite.vilib.util.SkullSetter;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The menu to select other players to spectate
|
||||
*/
|
||||
public class SpectatorMenu {
|
||||
|
||||
public void open(Player player) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
|
||||
PagedMenu spectator = new PagedMenu(3, Locales.getString(player, "play.spectator.name"));
|
||||
|
||||
List<MenuItem> display = new ArrayList<>();
|
||||
|
||||
for (Session session : Divider.sections.keySet()) {
|
||||
if (!session.isAcceptingSpectators()) { // only showcase sessions with spectators enabled
|
||||
continue;
|
||||
}
|
||||
|
||||
if (user != null && session == user.session) { // don't let player join their own session
|
||||
continue;
|
||||
}
|
||||
|
||||
if (session.getPlayers().isEmpty()) { // weird but possible
|
||||
continue;
|
||||
}
|
||||
|
||||
var pp = session.getPlayers().get(0);
|
||||
|
||||
Item item = Locales.getItem(locale, "play.spectator.head", pp.getName());
|
||||
// Player head gathering
|
||||
item.material(Material.PLAYER_HEAD);
|
||||
|
||||
ItemStack stack = item.build(); // Updating meta requires building
|
||||
stack.setType(Material.PLAYER_HEAD);
|
||||
|
||||
// bedrock has no player skull support
|
||||
if (!ParkourUser.isBedrockPlayer(player)) {
|
||||
if (pp.getName() != null && !pp.getName().startsWith(".")) { // bedrock players' names with geyser start with a .
|
||||
SkullMeta meta = (SkullMeta) stack.getItemMeta();
|
||||
|
||||
if (meta != null) {
|
||||
SkullSetter.setPlayerHead(pp.player, meta);
|
||||
item.meta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item.click(event -> Modes.SPECTATOR.create(player, session));
|
||||
|
||||
display.add(item);
|
||||
}
|
||||
|
||||
spectator.displayRows(0, 1)
|
||||
.addToDisplay(display)
|
||||
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> spectator.page(1)))
|
||||
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> spectator.page(-1)))
|
||||
.item(22, Locales.getItem(player, "other.close").click(event -> Menus.PLAY.open(event.getPlayer())))
|
||||
.open(player);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
46
src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java
Normal file
46
src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package dev.loki.loparkour.menu.settings;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.efnilite.vilib.inventory.PagedMenu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import org.bukkit.Material;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LangMenu {
|
||||
|
||||
/**
|
||||
* Opens the language menu
|
||||
*
|
||||
* @param user The ParkourPlayer instance
|
||||
*/
|
||||
public void open(ParkourPlayer user) {
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PagedMenu style = new PagedMenu(3, Locales.getString(user.locale, "settings.lang.name"));
|
||||
|
||||
List<MenuItem> items = new ArrayList<>();
|
||||
for (String lang : Locales.locales.keySet()) {
|
||||
Item item = new Item(Material.PAPER, "<#238681><bold>" + Locales.getString(lang, "name"));
|
||||
|
||||
items.add(item.click(event -> {
|
||||
user.locale = lang;
|
||||
user._locale = lang;
|
||||
Menus.SETTINGS.open(event.getPlayer());
|
||||
}));
|
||||
}
|
||||
|
||||
style.displayRows(0, 1).addToDisplay(items)
|
||||
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> style.page(1)))
|
||||
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> style.page(-1)))
|
||||
.item(22, Locales.getItem(user.locale, "other.close").click(event -> Menus.SETTINGS.open(event.getPlayer())))
|
||||
.open(user.player);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
package dev.loki.loparkour.menu.settings;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.menu.DynamicMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.style.RandomStyle;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.efnilite.vilib.fastboard.FastBoard;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.MenuClickEvent;
|
||||
import dev.efnilite.vilib.inventory.PagedMenu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import dev.efnilite.vilib.inventory.item.SliderItem;
|
||||
import dev.efnilite.vilib.inventory.item.TimedItem;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles all menu-related activities
|
||||
*
|
||||
* @author loki
|
||||
* @since v3.0.0
|
||||
*/
|
||||
public class ParkourSettingsMenu extends DynamicMenu {
|
||||
|
||||
public ParkourSettingsMenu(ParkourOption... disabled) {
|
||||
|
||||
// ---------- top row ----------
|
||||
|
||||
// styles
|
||||
registerMainItem(1, 0, (p, user) -> Locales.getItem(p, ParkourOption.STYLES.path + ".item", user instanceof ParkourPlayer player ? player.style : null).click(event -> {
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
openStyleMenu(player);
|
||||
}), player -> checkOptions(player, ParkourOption.STYLES, disabled));
|
||||
|
||||
// leads
|
||||
registerMainItem(1, 1, (p, user) -> {
|
||||
Item displayItem = Locales.getItem(p, ParkourOption.LEADS.path);
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return displayItem;
|
||||
}
|
||||
|
||||
List<Integer> leads = Option.POSSIBLE_LEADS;
|
||||
|
||||
SliderItem item = new SliderItem().initial(leads.indexOf(player.blockLead)); // initial value of the player
|
||||
|
||||
int slot = 0;
|
||||
for (int value : leads) {
|
||||
item.add(slot, displayItem.clone()
|
||||
.amount(value)
|
||||
.modifyLore(line -> line.replace("%s", Integer.toString(value))),
|
||||
event2 -> handleSettingChange(player, () -> player.blockLead = value));
|
||||
slot++;
|
||||
}
|
||||
return item;
|
||||
}, player -> checkOptions(player, ParkourOption.LEADS, disabled));
|
||||
|
||||
// schematics
|
||||
registerMainItem(1, 8, (p, user) -> {
|
||||
Item item = Locales.getItem(p, ParkourOption.SCHEMATICS.path);
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
List<Double> difficulties = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
|
||||
List<String> values = Locales.getStringList(user.locale, "%s.values".formatted(ParkourOption.SCHEMATICS.path));
|
||||
|
||||
if (!difficulties.contains(player.schematicDifficulty)) {
|
||||
player.schematicDifficulty = difficulties.get(0);
|
||||
}
|
||||
|
||||
return new SliderItem()
|
||||
.initial(difficulties.indexOf(player.schematicDifficulty))
|
||||
.add(0, item.clone()
|
||||
.material(Material.RED_STAINED_GLASS_PANE)
|
||||
.modifyLore(line -> line.replace("%s", values.get(0))),
|
||||
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.0))
|
||||
.add(1, item.clone()
|
||||
.material(Material.LIME_STAINED_GLASS_PANE)
|
||||
.modifyLore(line -> line.replace("%s", values.get(1))),
|
||||
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.25))
|
||||
.add(2, item.clone()
|
||||
.material(Material.YELLOW_STAINED_GLASS_PANE)
|
||||
.modifyLore(line -> line.replace("%s", values.get(2))),
|
||||
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.5))
|
||||
.add(3, item.clone()
|
||||
.material(Material.ORANGE_STAINED_GLASS_PANE)
|
||||
.modifyLore(line -> line.replace("%s", values.get(3))),
|
||||
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.75))
|
||||
.add(4, item.clone()
|
||||
.material(Material.SKELETON_SKULL)
|
||||
.modifyLore(line -> line.replace("%s", values.get(4))),
|
||||
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 1.0));
|
||||
}, player -> checkOptions(player, ParkourOption.SCHEMATICS, disabled));
|
||||
|
||||
// time
|
||||
registerMainItem(1, 9, (p, user) -> {
|
||||
Item item = Locales.getItem(p, ParkourOption.TIME.path);
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// Tick times start at 6:00 and total is 24.000.
|
||||
// Source: https://minecraft.fandom.com/wiki/Daylight_cycle?file=Day_Night_Clock_24h.png
|
||||
List<Integer> times = Arrays.asList(0, 6000, 12000, 18000); // 00:00 -> 6:00 -> 12:00 -> 18:00
|
||||
|
||||
var format = Config.CONFIG.getInt("options.time.format");
|
||||
|
||||
return new SliderItem()
|
||||
.initial(times.indexOf(player.selectedTime))
|
||||
.add(0, item.clone()
|
||||
.modifyLore(line -> line.replace("%s", format == 12 ? "12:00 AM" : "00:00")),
|
||||
event -> handleSettingChange(player, () -> player.selectedTime = 0))
|
||||
.add(1, item.clone()
|
||||
.modifyLore(line -> line.replace("%s", format == 12 ? "6:00 AM" : "6:00")),
|
||||
event -> handleSettingChange(player, () -> player.selectedTime = 6000))
|
||||
.add(2, item.clone()
|
||||
.modifyLore(line -> line.replace("%s", format == 12 ? "12:00 PM" : "12:00")),
|
||||
event -> handleSettingChange(player, () -> player.selectedTime = 12000))
|
||||
.add(3, item.clone()
|
||||
.modifyLore(line -> line.replace("%s", format == 12 ? "6:00 PM" : "18:00")),
|
||||
event -> handleSettingChange(player, () -> player.selectedTime = 18000));
|
||||
}, player -> checkOptions(player, ParkourOption.TIME, disabled));
|
||||
|
||||
// ---------- bottom row ----------
|
||||
|
||||
// show scoreboard
|
||||
registerMainItem(2, 0, (p, user) -> {
|
||||
Item item = Locales.getItem(p, ParkourOption.SCOREBOARD.path);
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return new SliderItem()
|
||||
.initial(player.showScoreboard ? 0 : 1)
|
||||
.add(0, item.clone()
|
||||
.material(Material.LIME_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
|
||||
event -> handleSettingChange(player, () -> {
|
||||
player.showScoreboard = true;
|
||||
player.board = new FastBoard(p);
|
||||
}))
|
||||
.add(1, item.clone()
|
||||
.material(Material.RED_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
|
||||
event -> handleSettingChange(player, () -> {
|
||||
player.showScoreboard = false;
|
||||
if (player.board != null && !player.board.isDeleted()) {
|
||||
player.board.delete();
|
||||
}
|
||||
}));
|
||||
}, player -> checkOptions(player, ParkourOption.SCOREBOARD, disabled) && Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD)));
|
||||
|
||||
// show fall message
|
||||
registerMainItem(2, 1, (p, user) -> {
|
||||
Item item = Locales.getItem(p, ParkourOption.FALL_MESSAGE.path + ".item");
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return new SliderItem()
|
||||
.initial(player.showFallMessage ? 0 : 1)
|
||||
.add(0, item.clone()
|
||||
.material(Material.LIME_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
|
||||
event -> handleSettingChange(player, () -> player.showFallMessage = true))
|
||||
.add(1, item.clone()
|
||||
.material(Material.RED_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
|
||||
event -> handleSettingChange(player, () -> player.showFallMessage = false));
|
||||
}, player -> checkOptions(player, ParkourOption.FALL_MESSAGE, disabled));
|
||||
|
||||
// show sound
|
||||
registerMainItem(2, 2, (p, user) -> {
|
||||
Item item = Locales.getItem(p, ParkourOption.PARTICLES.path);
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return new SliderItem()
|
||||
.initial(player.particles ? 0 : 1)
|
||||
.add(0, item.clone().material(Material.LIME_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
|
||||
event -> handleSettingChange(player, () -> player.particles = true))
|
||||
.add(1, item.clone()
|
||||
.material(Material.RED_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
|
||||
event -> handleSettingChange(player, () -> player.particles = false));
|
||||
}, player -> checkOptions(player, ParkourOption.PARTICLES, disabled));
|
||||
|
||||
// show sound
|
||||
registerMainItem(2, 3, (p, user) -> {
|
||||
Item item = Locales.getItem(p, ParkourOption.SOUND.path);
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return new SliderItem()
|
||||
.initial(player.sound ? 0 : 1)
|
||||
.add(0, item.clone()
|
||||
.material(Material.LIME_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
|
||||
event -> handleSettingChange(player, () -> player.sound = true))
|
||||
.add(1, item.clone()
|
||||
.material(Material.RED_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
|
||||
event -> handleSettingChange(player, () -> player.sound = false));
|
||||
}, player -> checkOptions(player, ParkourOption.SOUND, disabled));
|
||||
|
||||
// show special blocks
|
||||
registerMainItem(2, 4, (p, user) -> {
|
||||
Item item = Locales.getItem(p, ParkourOption.SPECIAL_BLOCKS.path);
|
||||
|
||||
if (!(user instanceof ParkourPlayer player)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return new SliderItem()
|
||||
.initial(player.useSpecialBlocks ? 0 : 1)
|
||||
.add(0, item.clone()
|
||||
.material(Material.LIME_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
|
||||
event -> handleScoreSettingChange(player, event, () -> player.useSpecialBlocks = true))
|
||||
.add(1, item.clone()
|
||||
.material(Material.RED_STAINED_GLASS_PANE)
|
||||
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
|
||||
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
|
||||
event -> handleScoreSettingChange(player, event, () -> player.useSpecialBlocks = false));
|
||||
}, player -> checkOptions(player, ParkourOption.SPECIAL_BLOCKS, disabled));
|
||||
|
||||
// Always allow closing of the menu
|
||||
registerMainItem(3, 10,
|
||||
(player, user) -> Locales.getItem(player, "other.close")
|
||||
.click(event -> Menus.SETTINGS.open(event.getPlayer())),
|
||||
player -> true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the main menu to a valid ParkourPlayer instance
|
||||
*
|
||||
* @param user The ParkourPlayer
|
||||
*/
|
||||
public void open(ParkourPlayer user) {
|
||||
display(user.player, new Menu(4, Locales.getString(user.locale, "settings.name"))
|
||||
.distributeRowEvenly(0, 1, 2, 3)
|
||||
.item(27, Locales.getItem(user.locale, "other.close").click(event -> Menus.SETTINGS.open(event.getPlayer()))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the style menu
|
||||
*
|
||||
* @param player The ParkourPlayer instance
|
||||
*/
|
||||
public void openStyleMenu(ParkourPlayer player) {
|
||||
// init menu
|
||||
PagedMenu menu = new PagedMenu(3, Locales.getString(player.locale, ParkourOption.STYLES.path + ".name"));
|
||||
|
||||
List<MenuItem> items = new ArrayList<>();
|
||||
for (Style style : Registry.getStyles()) {
|
||||
var name = style.getName();
|
||||
|
||||
var perm = "%s.%s".formatted(ParkourOption.STYLES.permission, name.toLowerCase());
|
||||
if (Config.CONFIG.getBoolean("permissions.per-style") && !player.player.hasPermission(perm.replace(" ", "."))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var category = style instanceof RandomStyle ? "random" : "incremental";
|
||||
|
||||
items.add(Locales.getItem(player.player, ParkourOption.STYLES.path + ".style_item", name, category)
|
||||
.material(style.getNext())
|
||||
.click(event -> {
|
||||
player.style = name;
|
||||
player.updateGeneratorSettings(player.session.generator);
|
||||
open(player);
|
||||
}));
|
||||
}
|
||||
|
||||
menu.displayRows(0, 1)
|
||||
.addToDisplay(items)
|
||||
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
|
||||
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
|
||||
.item(22, Locales.getItem(player.locale, "other.close").click(event -> open(player)))
|
||||
.open(player.player);
|
||||
}
|
||||
|
||||
private boolean handleSettingChange(ParkourPlayer player, Runnable onAllowed) {
|
||||
onAllowed.run();
|
||||
player.updateGeneratorSettings(player.session.generator);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleScoreSettingChange(ParkourPlayer player, MenuClickEvent event, Runnable onAllowed) {
|
||||
if (player.session.generator.score == 0) {
|
||||
return handleSettingChange(player, onAllowed);
|
||||
}
|
||||
|
||||
event.menu().item(event.slot(), new TimedItem(Locales.getItem(player.locale, "settings.parkour_settings.items.no_change").click((event1) -> {}), event, LoParkour.getPlugin()).stay(5 * 20));
|
||||
event.menu().updateItem(event.slot());
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// replaces true/false with a checkmark and cross
|
||||
private String getBooleanSymbol(ParkourUser user, boolean value) {
|
||||
return value ? Locales.getString(user.player, "settings.parkour_settings.enabled") : Locales.getString(user.player, "settings.parkour_settings.disabled");
|
||||
}
|
||||
|
||||
// check if option is allowed to be displayed
|
||||
private boolean checkOptions(Player player, ParkourOption option, ParkourOption[] disabled) {
|
||||
return !Arrays.asList(disabled).contains(option) && option.mayPerform(player);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package dev.loki.loparkour.menu.settings;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.DynamicMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.item.SliderItem;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The menu for settings
|
||||
*/
|
||||
public class SettingsMenu extends DynamicMenu {
|
||||
|
||||
public SettingsMenu() {
|
||||
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "settings.parkour_settings.item").click(event -> {
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(event.getPlayer());
|
||||
|
||||
if (pp != null) {
|
||||
pp.session.generator.menu(pp);
|
||||
}
|
||||
}), player -> ParkourOption.PARKOUR_SETTINGS.mayPerform(player) && ParkourPlayer.isPlayer(player));
|
||||
|
||||
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "settings.lang.item", user != null ? Locales.getString(user.locale, "name") : "?").click(event -> Menus.LANG.open(ParkourPlayer.getPlayer(event.getPlayer()))), player -> ParkourOption.LANG.mayPerform(player) && ParkourUser.isUser(player));
|
||||
|
||||
registerMainItem(1, 2, (player, user) -> {
|
||||
// user has to be not-null to see this item
|
||||
assert user != null;
|
||||
|
||||
List<String> values = Locales.getStringList(user.locale, "settings.chat.values");
|
||||
|
||||
return new SliderItem().initial(switch (user.chatType) {
|
||||
case LOBBY_ONLY -> 0;
|
||||
case PLAYERS_ONLY -> 1;
|
||||
case PUBLIC -> 2;
|
||||
}).add(0, Locales.getItem(player, "settings.chat").modifyLore(lore -> lore.replace("%s", values.get(0))), event -> { // lobby only
|
||||
ParkourUser u = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (u != null) {
|
||||
u.chatType = Session.ChatType.LOBBY_ONLY;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).add(1, Locales.getItem(player, "settings.chat").modifyLore(lore -> lore.replace("%s", values.get(1))), event -> { // players only
|
||||
ParkourUser u = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (u != null) {
|
||||
u.chatType = Session.ChatType.PLAYERS_ONLY;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).add(2, Locales.getItem(player, "settings.chat").modifyLore(lore -> lore.replace("%s", values.get(2))), event -> { // public
|
||||
ParkourUser u = ParkourUser.getUser(event.getPlayer());
|
||||
|
||||
if (u != null) {
|
||||
u.chatType = Session.ChatType.PUBLIC;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, player -> ParkourOption.CHAT.mayPerform(player) && ParkourUser.isUser(player));
|
||||
|
||||
registerMainItem(2, 0, (player, user) -> Locales.getItem(player, "other.close").click(event -> event.getPlayer().closeInventory()), player -> true);
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
display(player, new Menu(3, Locales.getString(player, "settings.name")).distributeRowsEvenly());
|
||||
}
|
||||
}
|
||||
55
src/main/java/dev/loki/loparkour/mode/DefaultMode.java
Normal file
55
src/main/java/dev/loki/loparkour/mode/DefaultMode.java
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* The default parkour mode
|
||||
*/
|
||||
public class DefaultMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.default");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("Joining is currently disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof DefaultMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(ParkourGenerator::new, null, null, player);
|
||||
}
|
||||
}
|
||||
38
src/main/java/dev/loki/loparkour/mode/Mode.java
Normal file
38
src/main/java/dev/loki/loparkour/mode/Mode.java
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Interface for all modes.
|
||||
* Every registered mode needs to inherit this class, because it needs identifying functions.
|
||||
*/
|
||||
public interface Mode {
|
||||
|
||||
/**
|
||||
* Method that gets called when a mode is clicked in the menu or joined using /parkour join.
|
||||
*
|
||||
* @param player The player.
|
||||
*/
|
||||
void create(Player player);
|
||||
|
||||
/**
|
||||
* @param locale The locale of the menu, used to adjust the name.
|
||||
* @return The item used in menus to show this mode. If this item is null, the mode won't be displayed.
|
||||
*/
|
||||
@Nullable Item getItem(String locale);
|
||||
|
||||
/**
|
||||
* @return The {@link Leaderboard} that belongs to this mode
|
||||
*/
|
||||
@Nullable Leaderboard getLeaderboard();
|
||||
|
||||
/**
|
||||
* @return The internal name used for this mode.
|
||||
*/
|
||||
@NotNull String getName();
|
||||
|
||||
}
|
||||
14
src/main/java/dev/loki/loparkour/mode/Modes.java
Normal file
14
src/main/java/dev/loki/loparkour/mode/Modes.java
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
|
||||
public class Modes {
|
||||
|
||||
public static DefaultMode DEFAULT;
|
||||
public static SpectatorMode SPECTATOR;
|
||||
|
||||
public static void init() {
|
||||
DEFAULT = (DefaultMode) Registry.getMode("default");
|
||||
SPECTATOR = (SpectatorMode) Registry.getMode("spectator");
|
||||
}
|
||||
}
|
||||
29
src/main/java/dev/loki/loparkour/mode/MultiMode.java
Normal file
29
src/main/java/dev/loki/loparkour/mode/MultiMode.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Class for modes featuring multiple players.
|
||||
*/
|
||||
public interface MultiMode extends Mode {
|
||||
|
||||
/**
|
||||
* Adds a player to this mode.
|
||||
*
|
||||
* @param player The player
|
||||
*/
|
||||
void join(Player player, Session session);
|
||||
|
||||
/**
|
||||
* Removes a player from this mode.
|
||||
*
|
||||
* @param player The player
|
||||
*/
|
||||
void leave(Player player, Session session);
|
||||
|
||||
/**
|
||||
* @return The max player count.
|
||||
*/
|
||||
int getMaxPlayers();
|
||||
}
|
||||
67
src/main/java/dev/loki/loparkour/mode/SpectatorMode.java
Normal file
67
src/main/java/dev/loki/loparkour/mode/SpectatorMode.java
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.util.Strings;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class SpectatorMode implements Mode {
|
||||
|
||||
@Override
|
||||
public @NotNull String getName() {
|
||||
return "spectator";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Leaderboard getLeaderboard() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
Menus.SPECTATOR.open(player);
|
||||
}
|
||||
|
||||
public void create(Player player, Session session) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage(Strings.colour("<red><bold>Joining is currently disabled."));
|
||||
return;
|
||||
}
|
||||
if (!ParkourOption.SPECTATOR.mayPerform(player)) {
|
||||
player.sendMessage(Locales.getString(player, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
ParkourSpectator spectator;
|
||||
|
||||
if (session.getPlayers().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
ParkourUser.unregister(user, false, false, false);
|
||||
spectator = new ParkourSpectator(player, session, user.previousData);
|
||||
} else {
|
||||
spectator = new ParkourSpectator(player, session, null);
|
||||
}
|
||||
|
||||
session.addSpectators(spectator);
|
||||
}
|
||||
}
|
||||
235
src/main/java/dev/loki/loparkour/player/ParkourPlayer.java
Normal file
235
src/main/java/dev/loki/loparkour/player/ParkourPlayer.java
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.Profile;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.MultiMode;
|
||||
import dev.loki.loparkour.player.data.PreviousData;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Subclass of {@link ParkourUser}. This class is used for players who are actively playing Parkour in any (default) mode.
|
||||
*
|
||||
* @author loki
|
||||
*/
|
||||
public class ParkourPlayer extends ParkourUser {
|
||||
|
||||
public static final Map<String, OptionContainer> PLAYER_COLUMNS = new HashMap<>();
|
||||
|
||||
static {
|
||||
PLAYER_COLUMNS.put("uuid", new OptionContainer(null, null));
|
||||
PLAYER_COLUMNS.put("style", new OptionContainer(ParkourOption.STYLES, (player, v) -> player.style = v));
|
||||
PLAYER_COLUMNS.put("blockLead", new OptionContainer(ParkourOption.LEADS, (player, v) -> player.blockLead = Integer.parseInt(v)));
|
||||
PLAYER_COLUMNS.put("useParticles", new OptionContainer(ParkourOption.PARTICLES, (player, v) -> player.particles = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("useSpecial", new OptionContainer(ParkourOption.SPECIAL_BLOCKS, (player, v) -> player.useSpecialBlocks = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("showFallMsg", new OptionContainer(ParkourOption.FALL_MESSAGE, (player, v) -> player.showFallMessage = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("showScoreboard", new OptionContainer(ParkourOption.SCOREBOARD, (player, v) -> player.showScoreboard = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("selectedTime", new OptionContainer(ParkourOption.TIME, (player, v) -> player.selectedTime = Integer.parseInt(v)));
|
||||
PLAYER_COLUMNS.put("collectedRewards", new OptionContainer(null, (player, v) -> {
|
||||
player.collectedRewards = new ArrayList<>();
|
||||
|
||||
if (!v.isEmpty()) {
|
||||
player.collectedRewards.addAll(Arrays.stream(v.replaceAll("[ \\[\\]]", "").split(","))
|
||||
.distinct()
|
||||
.toList());
|
||||
}
|
||||
}));
|
||||
PLAYER_COLUMNS.put("locale", new OptionContainer(ParkourOption.LANG, (player, v) -> {
|
||||
player._locale = v;
|
||||
player.locale = v;
|
||||
}));
|
||||
PLAYER_COLUMNS.put("schematicDifficulty", new OptionContainer(ParkourOption.SCHEMATICS, (player, v) -> player.schematicDifficulty = Double.parseDouble(v)));
|
||||
PLAYER_COLUMNS.put("sound", new OptionContainer(ParkourOption.SOUND, (player, v) -> player.sound = parseBoolean(v)));
|
||||
}
|
||||
|
||||
public @Expose Double schematicDifficulty;
|
||||
public @Expose Integer blockLead;
|
||||
public @Expose Boolean particles;
|
||||
public @Expose Boolean sound;
|
||||
public @Expose Boolean useSpecialBlocks;
|
||||
public @Expose Boolean showFallMessage;
|
||||
public @Expose Boolean showScoreboard;
|
||||
public @Expose Integer selectedTime;
|
||||
public @Expose String style;
|
||||
public @Expose String _locale;
|
||||
public @Expose List<String> collectedRewards;
|
||||
/**
|
||||
* Creates a new instance of a ParkourPlayer<br>
|
||||
* If you are using the API, please use {@link ParkourPlayer#register(Player, Session)} instead
|
||||
*/
|
||||
public ParkourPlayer(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
|
||||
super(player, session, previousData);
|
||||
|
||||
this._locale = locale;
|
||||
|
||||
// generic player settings
|
||||
player.setFlying(false);
|
||||
player.setAllowFlight(false);
|
||||
player.setInvisible(false);
|
||||
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean parseBoolean(String string) {
|
||||
return string == null
|
||||
|| string.equals("1") // for MySQL
|
||||
|| string.equals("true"); // for disk
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The player.
|
||||
* @return True when this player is a {@link ParkourPlayer}, false if not.
|
||||
*/
|
||||
public static boolean isPlayer(@Nullable Player player) {
|
||||
return player != null && getPlayers().stream().anyMatch(other -> other.player == player);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The player.
|
||||
* @return player as a {@link ParkourPlayer}, null if not found.
|
||||
*/
|
||||
public static @Nullable ParkourPlayer getPlayer(@NotNull Player player) {
|
||||
return getPlayers().stream()
|
||||
.filter(other -> other.getUUID() == player.getUniqueId())
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return List with all players.
|
||||
*/
|
||||
public static List<ParkourPlayer> getPlayers() {
|
||||
return Divider.sections.keySet().stream()
|
||||
.flatMap(session -> session.getPlayers().stream())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() {
|
||||
LoParkour.log("Unregistering player %s".formatted(player.getName()));
|
||||
|
||||
if (session.generator != null &&
|
||||
session.generator.getMode() != null &&
|
||||
session.generator.getMode() instanceof MultiMode mode) {
|
||||
mode.leave(player, session);
|
||||
}
|
||||
|
||||
session.removePlayers(this);
|
||||
|
||||
save(LoParkour.getPlugin().isEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's settings. If an item is not included, the setting gets reset.
|
||||
*
|
||||
* @param settings The settings map.
|
||||
*/
|
||||
public void setSettings(@NotNull Map<String, Object> settings) {
|
||||
for (String key : PLAYER_COLUMNS.keySet()) {
|
||||
Object value = settings.get(key);
|
||||
OptionContainer container = PLAYER_COLUMNS.get(key);
|
||||
|
||||
if (container.consumer == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value == null || !Option.OPTIONS_ENABLED.getOrDefault(container.option, true)) {
|
||||
container.consumer.accept(this, Option.OPTIONS_DEFAULTS.getOrDefault(container.option, ""));
|
||||
continue;
|
||||
}
|
||||
|
||||
container.consumer.accept(this, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces this player's generator to match the settings of this player.
|
||||
*/
|
||||
public void updateGeneratorSettings(ParkourGenerator generator) {
|
||||
Profile profile = generator.profile;
|
||||
|
||||
profile.set("schematicDifficulty", schematicDifficulty.toString())
|
||||
.set("blockLead", blockLead.toString())
|
||||
.set("particles", particles.toString())
|
||||
.set("sound", sound.toString())
|
||||
.set("useSpecialBlocks", useSpecialBlocks.toString())
|
||||
.set("showFallMessage", showFallMessage.toString())
|
||||
.set("showScoreboard", showScoreboard.toString())
|
||||
.set("selectedTime", selectedTime.toString())
|
||||
.set("style", style);
|
||||
|
||||
generator.overrideProfile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the player's data to their file
|
||||
*/
|
||||
public void save(boolean async) {
|
||||
Runnable write = () -> Storage.writePlayer(this);
|
||||
|
||||
if (async) {
|
||||
Task.create(LoParkour.getPlugin()).async().execute(write).run();
|
||||
} else {
|
||||
write.run();
|
||||
}
|
||||
}
|
||||
|
||||
public void setup(Location to) {
|
||||
LoParkour.log("Setting up player %s".formatted(player.getName()));
|
||||
|
||||
if (to != null) {
|
||||
teleport(to);
|
||||
}
|
||||
|
||||
player.setGameMode(GameMode.ADVENTURE);
|
||||
|
||||
// -= Inventory =-
|
||||
if (Config.CONFIG.getBoolean("options.inventory-handling")) {
|
||||
Task.create(LoParkour.getPlugin()).delay(5).execute(() -> {
|
||||
LoParkour.log("Setting up inventory for player %s".formatted(player.getName()));
|
||||
|
||||
player.getInventory().clear();
|
||||
|
||||
List<Item> items = new ArrayList<>();
|
||||
|
||||
if (ParkourOption.PLAY.mayPerform(player)) items.add(Locales.getItem(locale, "play.item"));
|
||||
if (ParkourOption.COMMUNITY.mayPerform(player)) items.add(Locales.getItem(locale, "community.item"));
|
||||
if (ParkourOption.SETTINGS.mayPerform(player)) items.add(Locales.getItem(locale, "settings.item"));
|
||||
if (ParkourOption.LOBBY.mayPerform(player)) items.add(Locales.getItem(locale, "lobby.item"));
|
||||
|
||||
if (ParkourOption.QUIT.mayPerform(player)) items.add(Locales.getItem(locale, "other.quit"));
|
||||
|
||||
List<Integer> slots = Menu.getEvenlyDistributedSlots(items.size());
|
||||
Colls.range(0, items.size()).forEach(idx -> player.getInventory().setItem(slots.get(idx), items.get(idx).build()));
|
||||
}).run();
|
||||
} else {
|
||||
sendTranslated("other.customize");
|
||||
}
|
||||
}
|
||||
|
||||
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||
|
||||
}
|
||||
}
|
||||
110
src/main/java/dev/loki/loparkour/player/ParkourSpectator.java
Normal file
110
src/main/java/dev/loki/loparkour/player/ParkourSpectator.java
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.event.ParkourSpectateEvent;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.player.data.PreviousData;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.util.Strings;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Class for spectators of a Session.
|
||||
*
|
||||
* @author loki
|
||||
*/
|
||||
public class ParkourSpectator extends ParkourUser {
|
||||
|
||||
private final BukkitTask closestChecker;
|
||||
/**
|
||||
* The closest player.
|
||||
*/
|
||||
@NotNull
|
||||
public ParkourPlayer closest;
|
||||
|
||||
public ParkourSpectator(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
|
||||
super(player, session, previousData);
|
||||
|
||||
this.closest = session.getPlayers().get(0);
|
||||
|
||||
new ParkourSpectateEvent(this).call();
|
||||
|
||||
Task.create(LoParkour.getPlugin())
|
||||
.delay(1)
|
||||
.execute(() -> {
|
||||
teleport(closest.getLocation());
|
||||
|
||||
sendTranslated("play.spectator.join");
|
||||
|
||||
player.setGameMode(GameMode.SPECTATOR);
|
||||
player.setAllowFlight(true);
|
||||
player.setFlying(true);
|
||||
if (ParkourUser.isBedrockPlayer(player)) { // bedrock has no spectator mode, so just make the player invisible
|
||||
player.setInvisible(true);
|
||||
player.setCollidable(false);
|
||||
}
|
||||
})
|
||||
.run();
|
||||
|
||||
closestChecker = Task.create(LoParkour.getPlugin())
|
||||
.async()
|
||||
.delay(1)
|
||||
.repeat(10)
|
||||
.execute(() -> {
|
||||
if (session.getPlayers().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
closest = session.getPlayers().stream()
|
||||
.min(Comparator.comparing(other -> other.getLocation().distanceSquared(player.getLocation()))) // x or x^2 doesn't matter in getting smallest
|
||||
.orElse(closest);
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the spectator's action bar, scoreboard and checks distance.
|
||||
*/
|
||||
public void update() {
|
||||
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(Strings.colour(Locales.getString(player, "play.spectator.action_bar"))));
|
||||
player.setGameMode(GameMode.SPECTATOR);
|
||||
updateScoreboard(session.generator);
|
||||
|
||||
// spectator is still being teleported to world
|
||||
if (closest.getLocation().getWorld() != player.getLocation().getWorld()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (closest.getLocation().distanceSquared(player.getLocation()) < 100 * 100) { // avoid sqrt
|
||||
return;
|
||||
}
|
||||
|
||||
teleport(closest.getLocation());
|
||||
if (player.getGameMode() != GameMode.SPECTATOR) { // if player isn't in spectator or is a bedrock player
|
||||
return;
|
||||
}
|
||||
|
||||
// if player is a spectator
|
||||
player.setSpectatorTarget(null);
|
||||
player.setSpectatorTarget(player.getSpectatorTarget());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the closest checker runnable.
|
||||
*/
|
||||
@Override
|
||||
public void unregister() {
|
||||
closestChecker.cancel();
|
||||
session.removeSpectators(this);
|
||||
player.setInvisible(false);
|
||||
}
|
||||
}
|
||||
343
src/main/java/dev/loki/loparkour/player/ParkourUser.java
Normal file
343
src/main/java/dev/loki/loparkour/player/ParkourUser.java
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.event.ParkourJoinEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourLeaveEvent;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.hook.FloodgateHook;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.data.PreviousData;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import dev.efnilite.vilib.fastboard.FastBoard;
|
||||
import dev.efnilite.vilib.util.Strings;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import me.clip.placeholderapi.PlaceholderAPI;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.ChannelNotRegisteredException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Superclass of every type of player. This encompasses every player currently in the Parkour world.
|
||||
* This includes active players ({@link ParkourPlayer}) and spectators ({@link ParkourSpectator}).
|
||||
*
|
||||
* @author loki
|
||||
*/
|
||||
public abstract class ParkourUser {
|
||||
|
||||
/**
|
||||
* Registers a player. This registers the player internally.
|
||||
* This automatically unregisters the player if it is already registered.
|
||||
*
|
||||
* @param player The player
|
||||
* @return the ParkourPlayer instance of the newly joined player
|
||||
*/
|
||||
public static @NotNull ParkourPlayer register(@NotNull Player player, @NotNull Session session) {
|
||||
PreviousData data = null;
|
||||
ParkourUser existing = getUser(player);
|
||||
|
||||
if (existing != null) {
|
||||
LoParkour.log("Registering player %s with existing data".formatted(player.getName()));
|
||||
|
||||
data = existing.previousData;
|
||||
unregister(existing, false, false, false);
|
||||
} else {
|
||||
LoParkour.log("Registering player %s".formatted(player.getName()));
|
||||
}
|
||||
ParkourPlayer pp = new ParkourPlayer(player, session, data);
|
||||
|
||||
// stats
|
||||
joinCount++;
|
||||
new ParkourJoinEvent(pp).call();
|
||||
|
||||
Storage.readPlayer(pp);
|
||||
return pp;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the same as {@link #leave(ParkourUser)}, but instead for a Bukkit player instance.
|
||||
*
|
||||
* @param player The Bukkit player instance that will be removed from the game if the player is active.
|
||||
* @see #leave(ParkourUser)
|
||||
*/
|
||||
public static void leave(@NotNull Player player) {
|
||||
ParkourUser user = getUser(player);
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
leave(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces user to leave. Follows behaviour of /parkour leave.
|
||||
*
|
||||
* @param user The user.
|
||||
*/
|
||||
public static void leave(@NotNull ParkourUser user) {
|
||||
unregister(user, true, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a Parkour user instance.
|
||||
*
|
||||
* @param user The user to unregister.
|
||||
* @param restorePreviousData Whether to restore the data from before the player joined the parkour.
|
||||
* @param kickIfBungee Whether to kick the player if Bungeecord mode is enabled.
|
||||
*/
|
||||
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
|
||||
new ParkourLeaveEvent(user).call();
|
||||
LoParkour.log("Unregistering player %s, restorePreviousData = %s, kickIfBungee = %s".formatted(user.getName(), restorePreviousData, kickIfBungee));
|
||||
|
||||
try {
|
||||
user.unregister();
|
||||
|
||||
if (user.board != null && !user.board.isDeleted()) {
|
||||
user.board.delete();
|
||||
}
|
||||
} catch (Exception ex) { // safeguard to prevent people from losing data
|
||||
LoParkour.logging().stack("Error while trying to make player %s leave".formatted(user.getName()), ex);
|
||||
user.send("<red><bold>There was an error while trying to handle leaving.");
|
||||
}
|
||||
|
||||
if (restorePreviousData && Config.CONFIG.getBoolean("bungeecord.enabled") && kickIfBungee) {
|
||||
sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!restorePreviousData) return;
|
||||
|
||||
user.previousData.apply(user.player, urgent);
|
||||
|
||||
Mode mode = user.session.generator.getMode();
|
||||
if (mode == null) {
|
||||
LoParkour.logging().error("Mode is null for %s".formatted(user.getName()));
|
||||
mode = Modes.DEFAULT;
|
||||
}
|
||||
|
||||
if (user instanceof ParkourPlayer player) {
|
||||
Mode finalMode = mode;
|
||||
user.previousData.onLeave.forEach(r -> r.execute(player, finalMode));
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a player to a BungeeCord server. server is the server name.
|
||||
private static void sendPlayerToServer(Player player, String server) {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Connect");
|
||||
out.writeUTF(server);
|
||||
|
||||
try {
|
||||
player.sendPluginMessage(LoParkour.getPlugin(), "BungeeCord", out.toByteArray());
|
||||
} catch (ChannelNotRegisteredException ex) {
|
||||
LoParkour.logging().stack("Error while trying to send %s to server %s. This server is not registered.".formatted(player.getName(), server), ex);
|
||||
player.kickPlayer("Couldn't move you to %s. Please rejoin.".formatted(server));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The player.
|
||||
* @return True when this player is a {@link ParkourUser}, false if not.
|
||||
*/
|
||||
public static boolean isUser(@Nullable Player player) {
|
||||
return player != null && getUsers().stream().anyMatch(other -> other.player == player);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The player.
|
||||
* @return player as a {@link ParkourUser}, null if not found.
|
||||
*/
|
||||
public static @Nullable ParkourUser getUser(@NotNull Player player) {
|
||||
return getUsers().stream()
|
||||
.filter(other -> other.getUUID() == player.getUniqueId())
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Set with all users.
|
||||
*/
|
||||
public static Set<ParkourUser> getUsers() {
|
||||
return Divider.sections.keySet().stream()
|
||||
.flatMap(session -> session.getUsers().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* This user's locale
|
||||
*/
|
||||
@NotNull
|
||||
public String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
|
||||
/**
|
||||
* This user's scoreboard
|
||||
*/
|
||||
public FastBoard board;
|
||||
|
||||
/**
|
||||
* This user's PreviousData
|
||||
*/
|
||||
@NotNull
|
||||
public PreviousData previousData;
|
||||
|
||||
/**
|
||||
* The selected {@link Session.ChatType}
|
||||
*/
|
||||
public Session.ChatType chatType = Session.ChatType.PUBLIC;
|
||||
|
||||
/**
|
||||
* The {@link Session} this user is in.
|
||||
*/
|
||||
public final Session session;
|
||||
|
||||
/**
|
||||
* The Bukkit player instance associated with this user.
|
||||
*/
|
||||
public final Player player;
|
||||
|
||||
/**
|
||||
* The {@link Instant} when the player joined.
|
||||
*/
|
||||
public final Instant joined;
|
||||
|
||||
/**
|
||||
* The amount of players that have joined while the plugin has been enabled.
|
||||
*/
|
||||
public static int joinCount;
|
||||
|
||||
public ParkourUser(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
|
||||
this.player = player;
|
||||
this.session = session;
|
||||
this.joined = Instant.now();
|
||||
this.previousData = previousData == null ? new PreviousData(player) : previousData;
|
||||
|
||||
if (Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD))) {
|
||||
this.board = new FastBoard(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this user.
|
||||
*/
|
||||
protected abstract void unregister();
|
||||
|
||||
/**
|
||||
* Teleports the player asynchronously.
|
||||
*
|
||||
* @param to Where the player will be teleported to
|
||||
*/
|
||||
public void teleport(@NotNull Location to) {
|
||||
PaperLib.teleportAsync(player, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message.
|
||||
*
|
||||
* @param message The message
|
||||
*/
|
||||
public void send(String message) {
|
||||
player.sendMessage(Strings.colour(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a translated message
|
||||
*
|
||||
* @param key The translation key
|
||||
* @param format Any objects that may be given to the formatting of the string.
|
||||
*/
|
||||
public void sendTranslated(String key, Object... format) {
|
||||
send(Locales.getString(locale, key).formatted(format));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the scoreboard for the specified generator.
|
||||
*
|
||||
* @param generator The generator.
|
||||
*/
|
||||
public void updateScoreboard(ParkourGenerator generator) {
|
||||
// board can be null a few ticks after on player leave
|
||||
if (board == null || board.isDeleted() || !generator.profile.get("showScoreboard").asBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Leaderboard leaderboard = generator.getMode().getLeaderboard();
|
||||
Score top = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.getScoreAtRank(1);
|
||||
Score high = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.get(getUUID());
|
||||
if (top == null) {
|
||||
top = new Score("?", "?", "?", 0);
|
||||
}
|
||||
|
||||
board.updateTitle(replace(Locales.getString(locale, "scoreboard.title"), top, high, generator));
|
||||
board.updateLines(replace(Locales.getStringList(locale, "scoreboard.lines"), top, high, generator));
|
||||
}
|
||||
|
||||
private List<String> replace(List<String> s, Score top, Score high, ParkourGenerator generator) {
|
||||
return s.stream().map(line -> replace(line, top, high, generator)).toList();
|
||||
}
|
||||
|
||||
private String replace(String s, Score top, Score high, ParkourGenerator generator) {
|
||||
return Strings.colour(translate(player, s)
|
||||
.replace("%score%", Integer.toString(generator.score))
|
||||
.replace("%time%", generator.getFormattedTime())
|
||||
.replace("%difficulty%", Double.toString(generator.getDifficultyScore()))
|
||||
|
||||
.replace("%top_score%", Integer.toString(top.score()))
|
||||
.replace("%top_player%", top.name())
|
||||
.replace("%top_time%", top.time())
|
||||
|
||||
.replace("%high_score%", Integer.toString(high.score()))
|
||||
.replace("%high_score_time%", high.time()));
|
||||
}
|
||||
|
||||
// translate papi
|
||||
private String translate(Player player, String string) {
|
||||
return LoParkour.getPlaceholderHook() == null ? string : PlaceholderAPI.setPlaceholders(player, string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The player's uuid
|
||||
*/
|
||||
public UUID getUUID() {
|
||||
return player.getUniqueId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The player's location
|
||||
*/
|
||||
public Location getLocation() {
|
||||
return player.getLocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The player's name
|
||||
*/
|
||||
public String getName() {
|
||||
return player.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The player
|
||||
* @return true if the player is a Bedrock player, false if not.
|
||||
*/
|
||||
public static boolean isBedrockPlayer(Player player) {
|
||||
return Bukkit.getPluginManager().isPluginEnabled("floodgate") && FloodgateHook.isBedrockPlayer(player);
|
||||
}
|
||||
}
|
||||
106
src/main/java/dev/loki/loparkour/player/data/InventoryData.java
Normal file
106
src/main/java/dev/loki/loparkour/player/data/InventoryData.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package dev.loki.loparkour.player.data;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class InventoryData {
|
||||
|
||||
private final File file;
|
||||
private final Player player;
|
||||
private Map<Integer, ItemStack> items = new HashMap<>();
|
||||
|
||||
public InventoryData(Player player) {
|
||||
this.player = player;
|
||||
this.file = LoParkour.getInFolder("inventories/%s".formatted(player.getUniqueId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives all items to the player.
|
||||
*/
|
||||
public void apply() {
|
||||
player.getInventory().clear();
|
||||
items.forEach((slot, item) -> player.getInventory().setItem(slot, item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads inventory data from file.
|
||||
*
|
||||
* @param onFinish What to do when the async procedure has finished.
|
||||
*/
|
||||
public void load(Consumer<@Nullable InventoryData> onFinish) {
|
||||
if (!file.exists()) {
|
||||
onFinish.accept(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Task.create(LoParkour.getPlugin()).async().execute(() -> loadFile(onFinish)).run();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void loadFile(Consumer<@Nullable InventoryData> onFinish) {
|
||||
try (BukkitObjectInputStream stream = new BukkitObjectInputStream(new BufferedInputStream(new FileInputStream(file)))) {
|
||||
items = (Map<Integer, ItemStack>) stream.readObject();
|
||||
|
||||
onFinish.accept(this);
|
||||
} catch (IOException | ClassNotFoundException ex) {
|
||||
LoParkour.logging().stack("Error while reading inventory of %s from file %s".formatted(player.getName(), file.getName()), ex);
|
||||
onFinish.accept(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the inventory to cache, so if the player leaves the player gets their items back
|
||||
*
|
||||
* @param toFile Whether the file should be updated.
|
||||
*/
|
||||
public void save(boolean toFile) {
|
||||
int index = 0;
|
||||
|
||||
Inventory inventory = this.player.getInventory();
|
||||
for (ItemStack item : inventory.getContents()) {
|
||||
if (item != null) {
|
||||
this.items.put(index, item);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
String command = Config.CONFIG.getString("options.alt-inventory-saving-command");
|
||||
if (!command.isEmpty()) {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command.replace("%player%", player.getName()));
|
||||
}
|
||||
|
||||
if (toFile) {
|
||||
Task.create(LoParkour.getPlugin()).async().execute(this::saveFile).run();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFile() {
|
||||
try {
|
||||
file.getParentFile().mkdirs();
|
||||
file.createNewFile();
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while creating file to save inventory of %s to file %s".formatted(player.getName(), file.getName()), ex);
|
||||
}
|
||||
|
||||
try (ObjectOutputStream stream = new BukkitObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
stream.writeObject(items);
|
||||
stream.flush();
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while saving inventory of %s to file %s".formatted(player.getName(), file.getName()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package dev.loki.loparkour.player.data;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.reward.Reward;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class for storing previous data of players
|
||||
*/
|
||||
public class PreviousData {
|
||||
|
||||
private final InventoryData inventoryData;
|
||||
|
||||
private final int hunger;
|
||||
private final boolean flying;
|
||||
private final boolean allowFlight;
|
||||
private final GameMode gamemode;
|
||||
private final Location location;
|
||||
private final Collection<PotionEffect> effects;
|
||||
|
||||
/**
|
||||
* List of all {@link Reward} to execute on leave.
|
||||
*/
|
||||
public List<Reward> onLeave = new ArrayList<>();
|
||||
|
||||
public PreviousData(@NotNull Player player) {
|
||||
gamemode = player.getGameMode();
|
||||
location = player.getLocation();
|
||||
hunger = player.getFoodLevel();
|
||||
allowFlight = player.getAllowFlight();
|
||||
flying = player.isFlying();
|
||||
effects = player.getActivePotionEffects();
|
||||
|
||||
for (PotionEffect effect : effects) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("options.inventory-handling")) {
|
||||
inventoryData = new InventoryData(player);
|
||||
inventoryData.save(Config.CONFIG.getBoolean("options.inventory-saving"));
|
||||
} else {
|
||||
inventoryData = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void apply(Player player, boolean urgent) {
|
||||
var to = Config.CONFIG.getBoolean("bungeecord.go-back-enabled") ? Option.GO_BACK_LOC : location;
|
||||
|
||||
if (!urgent)
|
||||
PaperLib.teleportAsync(player, to).thenRun(() -> apply(player));
|
||||
else {
|
||||
player.teleport(to);
|
||||
|
||||
apply(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void apply(Player player) {
|
||||
try {
|
||||
player.setFoodLevel(hunger);
|
||||
player.setGameMode(gamemode);
|
||||
player.setAllowFlight(allowFlight);
|
||||
player.setFlying(flying);
|
||||
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
|
||||
player.resetPlayerTime();
|
||||
player.resetPlayerWeather();
|
||||
player.setVelocity(new Vector(0, 0, 0));
|
||||
player.setFallDistance(0f);
|
||||
} catch (Exception ex) { // not the best way to do this... too bad!
|
||||
LoParkour.logging().stack("Error while recovering stats of %s".formatted(player.getName()), ex);
|
||||
}
|
||||
|
||||
if (inventoryData != null) {
|
||||
inventoryData.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/main/java/dev/loki/loparkour/reward/Reward.java
Normal file
71
src/main/java/dev/loki/loparkour/reward/Reward.java
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package dev.loki.loparkour.reward;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.hook.VaultHook;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A class for handling reward commands, etc.
|
||||
*/
|
||||
// todo add cross-server support
|
||||
// todo mode-specific way of saving one-time-rewards
|
||||
public record Reward(@NotNull String string) {
|
||||
|
||||
/**
|
||||
* Parses and executes this reward
|
||||
*
|
||||
* @param player The player to which to give this reward to
|
||||
*/
|
||||
public void execute(@NotNull ParkourPlayer player, @NotNull Mode mode) {
|
||||
if (string.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String string = this.string;
|
||||
|
||||
Mode rewardMode = Registry.getModes().stream()
|
||||
.filter(m -> this.string.contains("%s:".formatted(m.getName().toLowerCase())))
|
||||
.findFirst()
|
||||
.orElse(Modes.DEFAULT);
|
||||
|
||||
if (mode != rewardMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
string = string.replaceFirst("%s:".formatted(rewardMode.getName().toLowerCase()), "");
|
||||
|
||||
// check for placeholders
|
||||
if (string.toLowerCase().contains("%player%")) {
|
||||
string = string.replaceAll("%player%", player.getName());
|
||||
}
|
||||
|
||||
// check for extra data
|
||||
if (string.toLowerCase().contains("leave:")) { // leave:
|
||||
string = string.replaceFirst("leave:", "");
|
||||
player.previousData.onLeave.add(new Reward(string));
|
||||
return;
|
||||
}
|
||||
|
||||
// check for things to perform
|
||||
if (string.toLowerCase().contains("send:")) {
|
||||
string = string.replaceFirst("send:", "");
|
||||
|
||||
player.send(string);
|
||||
} else if (string.toLowerCase().contains("vault:")) {
|
||||
string = string.replaceFirst("vault:", "");
|
||||
|
||||
try {
|
||||
VaultHook.deposit(player.player, Double.parseDouble(string));
|
||||
} catch (NumberFormatException ex) {
|
||||
LoParkour.logging().stack("Error while trying to process Vault reward", "check your rewards file for incorrect numbers", ex);
|
||||
}
|
||||
} else {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), string);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/main/java/dev/loki/loparkour/reward/Rewards.java
Normal file
79
src/main/java/dev/loki/loparkour/reward/Rewards.java
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package dev.loki.loparkour.reward;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class that reads the rewards-v2.yml file and puts them in the variables listed below.
|
||||
*/
|
||||
public class Rewards {
|
||||
|
||||
public static boolean REWARDS_ENABLED;
|
||||
|
||||
/**
|
||||
* A map with all Score-type score rewards.
|
||||
* The key is the score, and the value are the commands that will be executed once this score is reached.
|
||||
*/
|
||||
public static Map<Integer, List<Reward>> SCORE_REWARDS = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A map with all Interval-type score rewards.
|
||||
* The key is the score, and the value are the commands that will be executed once this score is reached.
|
||||
*/
|
||||
public static Map<Integer, List<Reward>> INTERVAL_REWARDS = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A map with all One time-type score rewards.
|
||||
* The key is the score, and the value are the commands that will be executed once this score is reached.
|
||||
*/
|
||||
public static Map<Integer, List<Reward>> ONE_TIME_REWARDS = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Reads the rewards from the rewards-v2.yml file
|
||||
*/
|
||||
public static void init() {
|
||||
// init options
|
||||
REWARDS_ENABLED = Config.REWARDS.getBoolean("enabled");
|
||||
|
||||
if (!REWARDS_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
SCORE_REWARDS = parseScores("score-rewards");
|
||||
INTERVAL_REWARDS = parseScores("interval-rewards");
|
||||
ONE_TIME_REWARDS = parseScores("one-time-rewards");
|
||||
}
|
||||
|
||||
private static Map<Integer, List<Reward>> parseScores(String path) {
|
||||
Map<Integer, List<Reward>> rewardMap = new HashMap<>();
|
||||
|
||||
for (String score : Config.REWARDS.getChildren(path)) {
|
||||
|
||||
// read commands for this score
|
||||
List<Reward> rewardStrings = Config.REWARDS.getStringList("%s.%s".formatted(path, score)).stream()
|
||||
.map(Reward::new)
|
||||
.toList();
|
||||
|
||||
try {
|
||||
int value = Integer.parseInt(score);
|
||||
|
||||
if (value < 1) {
|
||||
LoParkour.logging().stack("Error while trying to read rewards",
|
||||
"check the rewards file for incorrect numbers",
|
||||
new IllegalArgumentException("%s is not a valid score".formatted(value)));
|
||||
continue;
|
||||
}
|
||||
|
||||
rewardMap.put(value, rewardStrings);
|
||||
} catch (NumberFormatException ex) {
|
||||
LoParkour.logging().stack("Error while trying to read rewards", "check the rewards file for incorrect numbers", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return rewardMap;
|
||||
}
|
||||
}
|
||||
56
src/main/java/dev/loki/loparkour/schematic/Schematics.java
Normal file
56
src/main/java/dev/loki/loparkour/schematic/Schematics.java
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package dev.loki.loparkour.schematic;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Stores schematics, so they don't have to be read every time.
|
||||
*/
|
||||
public class Schematics {
|
||||
|
||||
private static final String[] SPAWN_SCHEMATICS = new String[]{
|
||||
"spawn-island", "spawn-island-duels"
|
||||
};
|
||||
private static final File FOLDER = LoParkour.getInFolder("schematics");
|
||||
|
||||
/**
|
||||
* Reads all files.
|
||||
*/
|
||||
public static void init() {
|
||||
Task.create(LoParkour.getPlugin()).async().execute(() -> {
|
||||
if (!FOLDER.exists()) {
|
||||
FOLDER.mkdirs();
|
||||
}
|
||||
|
||||
File[] files = FOLDER.listFiles((dir, name) -> name.contains("parkour-") || name.contains("spawn-island"));
|
||||
|
||||
if (files == null || files.length == 0) {
|
||||
download();
|
||||
init();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dev.efnilite.vilib.schematic.Schematics.addFromFiles(LoParkour.getPlugin(), files);
|
||||
} catch (IOException | ClassNotFoundException ex) {
|
||||
LoParkour.logging().stack("Error while trying to load schematics", ex);
|
||||
}
|
||||
}).run();
|
||||
}
|
||||
|
||||
private static void download() {
|
||||
LoParkour.log("Downloading schematics");
|
||||
|
||||
List<String> schematics = new ArrayList<>();
|
||||
schematics.addAll(Arrays.asList(SPAWN_SCHEMATICS));
|
||||
schematics.addAll(Config.SCHEMATICS.getChildren("difficulty", false).stream().map("parkour-%s"::formatted).toList());
|
||||
schematics.forEach(file -> LoParkour.getPlugin().saveResource("schematics/%s".formatted(file), true));
|
||||
}
|
||||
}
|
||||
293
src/main/java/dev/loki/loparkour/session/Session.java
Normal file
293
src/main/java/dev/loki/loparkour/session/Session.java
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
package dev.loki.loparkour.session;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* <p>A session is bound to a {@link Divider} section.
|
||||
* It manages all players, all spectators, visibility, the generator, etc.</p>
|
||||
* <p>Iteration 2.</p>
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class Session {
|
||||
|
||||
/**
|
||||
* The spawn location of this session.
|
||||
*/
|
||||
private Location spawnLocation;
|
||||
|
||||
/**
|
||||
* The generator.
|
||||
*/
|
||||
public ParkourGenerator generator;
|
||||
|
||||
/**
|
||||
* The visibility of this session. Default public.
|
||||
*/
|
||||
private Visibility visibility = Visibility.PUBLIC;
|
||||
|
||||
/**
|
||||
* List of muted users.
|
||||
*/
|
||||
private final List<ParkourUser> muted = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* List of users.
|
||||
*/
|
||||
private final Map<UUID, ParkourUser> users = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Function that takes the current session and returns whether new players should be accepted.
|
||||
*/
|
||||
private Function<Session, Boolean> isAcceptingPlayers = session -> false;
|
||||
|
||||
/**
|
||||
* Function that takes the current session and returns whether new spectators should be accepted.
|
||||
*/
|
||||
private Function<Session, Boolean> isAcceptingSpectators = session -> session.visibility == Visibility.PUBLIC;
|
||||
|
||||
/**
|
||||
* Creates a new session.
|
||||
*
|
||||
* @param generatorFunction The generator function.
|
||||
* @param isAcceptingPlayers The function that takes the current session and returns whether new players should be accepted.
|
||||
* @param isAcceptingSpectators The function that takes the current session and returns whether new spectators should be accepted.
|
||||
* @param players The players.
|
||||
* @return The session.
|
||||
*/
|
||||
public static Session create(Function<Session, ParkourGenerator> generatorFunction,
|
||||
Function<Session, Boolean> isAcceptingPlayers,
|
||||
Function<Session, Boolean> isAcceptingSpectators,
|
||||
Player... players) {
|
||||
LoParkour.log("Creating session");
|
||||
|
||||
if (players != null) {
|
||||
LoParkour.log("Players in session: %s".formatted(Arrays.stream(players).map(Player::getName).toList()));
|
||||
}
|
||||
|
||||
Session session = new Session();
|
||||
|
||||
if (isAcceptingPlayers != null) session.isAcceptingPlayers = isAcceptingPlayers;
|
||||
if (isAcceptingSpectators != null) session.isAcceptingSpectators = isAcceptingSpectators;
|
||||
|
||||
List<ParkourPlayer> pps = new ArrayList<>();
|
||||
if (players != null) {
|
||||
for (Player player : players) {
|
||||
ParkourPlayer pp = ParkourUser.register(player, session);
|
||||
session.addPlayers(pp);
|
||||
pps.add(pp);
|
||||
}
|
||||
}
|
||||
|
||||
session.spawnLocation = Divider.add(session);
|
||||
session.generator = generatorFunction.apply(session);
|
||||
|
||||
if (players != null) {
|
||||
pps.forEach(p -> p.updateGeneratorSettings(session.generator));
|
||||
}
|
||||
|
||||
session.generator.island.build(session.spawnLocation);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of this session.
|
||||
* @param visibility The visibility.
|
||||
*/
|
||||
public void setVisibility(Visibility visibility) {
|
||||
this.visibility = visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The visibility of this session.
|
||||
*/
|
||||
public Visibility getVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds provided players to this session's player list.
|
||||
*
|
||||
* @param toAdd The players to add.
|
||||
*/
|
||||
public void addPlayers(ParkourPlayer... toAdd) {
|
||||
for (ParkourPlayer player : toAdd) {
|
||||
LoParkour.log("Adding player %s to session".formatted(player.getName()));
|
||||
|
||||
for (ParkourPlayer to : getPlayers()) {
|
||||
to.send(Locales.getString(player.locale, "lobby.other_join").formatted(player.getName()));
|
||||
}
|
||||
|
||||
users.put(player.getUUID(), player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes provided players from this session's player list.
|
||||
*
|
||||
* @param toRemove The players to remove.
|
||||
*/
|
||||
public void removePlayers(ParkourPlayer... toRemove) {
|
||||
for (ParkourPlayer player : toRemove) {
|
||||
LoParkour.log("Removing player %s from session".formatted(player.getName()));
|
||||
|
||||
users.remove(player.getUUID());
|
||||
}
|
||||
|
||||
List<ParkourPlayer> players = getPlayers();
|
||||
for (ParkourPlayer player : toRemove) {
|
||||
for (ParkourPlayer to : players) {
|
||||
to.send(Locales.getString(player.locale, "lobby.other_leave").formatted(player.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.length > 0 && players.isEmpty()) {
|
||||
generator.reset(false);
|
||||
Divider.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The players.
|
||||
*/
|
||||
public List<ParkourPlayer> getPlayers() {
|
||||
return users.values().stream()
|
||||
.filter(user -> user instanceof ParkourPlayer)
|
||||
.map(user -> (ParkourPlayer) user)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds provided spectators to this session's spectator list.
|
||||
*
|
||||
* @param spectators The spectators to add.
|
||||
*/
|
||||
public void addSpectators(ParkourSpectator... spectators) {
|
||||
for (ParkourSpectator spectator : spectators) {
|
||||
LoParkour.log("Adding spectator %s to session".formatted(spectator.getName()));
|
||||
|
||||
for (ParkourPlayer player : getPlayers()) {
|
||||
player.sendTranslated("play.spectator.other_join", spectator.getName());
|
||||
}
|
||||
|
||||
users.put(spectator.getUUID(), spectator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes provided spectators from this session's spectator list.
|
||||
*
|
||||
* @param spectators The spectators to remove.
|
||||
*/
|
||||
public void removeSpectators(ParkourSpectator... spectators) {
|
||||
for (ParkourSpectator spectator : spectators) {
|
||||
LoParkour.log("Removing spectator %s from session".formatted(spectator.getName()));
|
||||
|
||||
for (ParkourPlayer player : getPlayers()) {
|
||||
player.sendTranslated("play.spectator.other_leave", spectator.getName());
|
||||
}
|
||||
|
||||
users.remove(spectator.getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The spectators.
|
||||
*/
|
||||
public List<ParkourSpectator> getSpectators() {
|
||||
return users.values().stream()
|
||||
.filter(user -> user instanceof ParkourSpectator)
|
||||
.map(user -> (ParkourSpectator) user)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The users.
|
||||
*/
|
||||
public List<ParkourUser> getUsers() {
|
||||
return new ArrayList<>(users.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the spawn location for this {@link Session}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public Location getSpawnLocation() {
|
||||
return spawnLocation.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles mute for the specified user.
|
||||
*
|
||||
* @param user The user to (un)mute.
|
||||
*/
|
||||
public void toggleMute(@NotNull ParkourUser user) {
|
||||
if (!muted.remove(user)) {
|
||||
muted.add(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param user The user.
|
||||
* @return True when the user is muted, false if not.
|
||||
*/
|
||||
public boolean isMuted(@NotNull ParkourUser user) {
|
||||
return muted.contains(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True when players may join this session, false if not.
|
||||
*/
|
||||
public boolean isAcceptingPlayers() {
|
||||
return isAcceptingPlayers.apply(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True when spectators may join this session, false if not.
|
||||
*/
|
||||
public boolean isAcceptingSpectators() {
|
||||
return isAcceptingSpectators.apply(this);
|
||||
}
|
||||
|
||||
public enum Visibility {
|
||||
|
||||
/**
|
||||
* No-one can join.
|
||||
*/
|
||||
PRIVATE,
|
||||
|
||||
/**
|
||||
* Only people with the session id can join.
|
||||
*/
|
||||
ID_ONLY,
|
||||
|
||||
/**
|
||||
* Anyone can join.
|
||||
*/
|
||||
PUBLIC,
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum for all available chat types that a player can select while playing
|
||||
*/
|
||||
public enum ChatType {
|
||||
|
||||
LOBBY_ONLY, PLAYERS_ONLY, PUBLIC
|
||||
|
||||
}
|
||||
}
|
||||
84
src/main/java/dev/loki/loparkour/storage/Storage.java
Normal file
84
src/main/java/dev/loki/loparkour/storage/Storage.java
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package dev.loki.loparkour.storage;
|
||||
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* I/O handling.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class Storage {
|
||||
|
||||
public static void init(String mode) {
|
||||
if (Option.SQL) {
|
||||
StorageSQL.init(mode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
if (Option.SQL) {
|
||||
StorageSQL.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads scores.
|
||||
*
|
||||
* @param mode The mode.
|
||||
* @return Map with all scores, unsorted.
|
||||
*/
|
||||
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
|
||||
if (Option.SQL) {
|
||||
return StorageSQL.readScores(mode);
|
||||
} else {
|
||||
return StorageDisk.readScores(mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes scores.
|
||||
*
|
||||
* @param mode The mode.
|
||||
* @param scores The score map.
|
||||
*/
|
||||
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
||||
if (Option.SQL) {
|
||||
StorageSQL.writeScores(mode, scores);
|
||||
} else {
|
||||
StorageDisk.writeScores(mode, scores);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads player data and applies changes.
|
||||
*
|
||||
* @param player The player.
|
||||
*/
|
||||
public static void readPlayer(@NotNull ParkourPlayer player) {
|
||||
if (Option.SQL) {
|
||||
StorageSQL.readPlayer(player);
|
||||
} else {
|
||||
StorageDisk.readPlayer(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes player data.
|
||||
*
|
||||
* @param player The player.
|
||||
*/
|
||||
public static void writePlayer(@NotNull ParkourPlayer player) {
|
||||
if (Option.SQL) {
|
||||
StorageSQL.writePlayer(player);
|
||||
} else {
|
||||
StorageDisk.writePlayer(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/main/java/dev/loki/loparkour/storage/StorageDisk.java
Normal file
133
src/main/java/dev/loki/loparkour/storage/StorageDisk.java
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package dev.loki.loparkour.storage;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Local disk (json) storage manager.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class StorageDisk {
|
||||
|
||||
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
|
||||
File file = getLeaderboardFile(mode);
|
||||
|
||||
if (!file.exists()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
try (FileReader reader = new FileReader(file)) {
|
||||
LeaderboardContainer read = LoParkour.getGson().fromJson(reader, LeaderboardContainer.class);
|
||||
|
||||
if (read == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
Map<UUID, String> serialized = new LinkedHashMap<>(read.serialized);
|
||||
Map<UUID, Score> scores = new HashMap<>();
|
||||
|
||||
serialized.forEach((uuid, score) -> scores.put(uuid, Score.fromString(score)));
|
||||
|
||||
return scores;
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while trying to read leaderboard file %s".formatted(mode), ex);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
||||
LeaderboardContainer container = new LeaderboardContainer();
|
||||
scores.forEach((uuid, score) -> container.serialized.put(uuid, score.toString()));
|
||||
|
||||
File file = getLeaderboardFile(mode);
|
||||
createFile(file);
|
||||
|
||||
try (FileWriter writer = new FileWriter(file)) {
|
||||
LoParkour.getGson().toJson(container, writer);
|
||||
writer.flush();
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while trying to write to leaderboard file %s".formatted(mode), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getLeaderboardFile(String mode) {
|
||||
return LoParkour.getInFolder("leaderboards/%s.json".formatted(mode.toLowerCase()));
|
||||
}
|
||||
|
||||
public static class LeaderboardContainer {
|
||||
@Expose
|
||||
public final Map<UUID, String> serialized = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
public static void readPlayer(@NotNull ParkourPlayer player) {
|
||||
if (!getPlayerFile(player).exists()) {
|
||||
player.setSettings(new HashMap<>());
|
||||
return;
|
||||
}
|
||||
|
||||
try (FileReader reader = new FileReader(getPlayerFile(player))) {
|
||||
ParkourPlayer from = LoParkour.getGson().fromJson(reader, ParkourPlayer.class);
|
||||
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
settings.put("style", from.style);
|
||||
settings.put("blockLead", from.blockLead);
|
||||
settings.put("useParticles", from.particles);
|
||||
settings.put("useSpecial", from.useSpecialBlocks);
|
||||
settings.put("showFallMsg", from.showFallMessage);
|
||||
settings.put("showScoreboard", from.showScoreboard);
|
||||
settings.put("selectedTime", from.selectedTime);
|
||||
settings.put("collectedRewards", from.collectedRewards);
|
||||
settings.put("locale", from._locale);
|
||||
settings.put("schematicDifficulty", from.schematicDifficulty);
|
||||
settings.put("sound", from.sound);
|
||||
|
||||
player.setSettings(settings);
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while trying to read disk data of %s".formatted(player.getName()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writePlayer(@NotNull ParkourPlayer player) {
|
||||
File file = getPlayerFile(player);
|
||||
|
||||
createFile(file);
|
||||
|
||||
try (FileWriter writer = new FileWriter(file)) {
|
||||
LoParkour.getGson().toJson(player, writer);
|
||||
writer.flush();
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while trying to write disk data of %s to file %s".formatted(player.getName(), file), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createFile(File file) {
|
||||
if (file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
file.getParentFile().mkdirs();
|
||||
file.createNewFile();
|
||||
} catch (IOException ex) {
|
||||
LoParkour.logging().stack("Error while trying to create file %s".formatted(file), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getPlayerFile(ParkourPlayer player) {
|
||||
return LoParkour.getInFolder("players/%s.json".formatted(player.getUUID()));
|
||||
}
|
||||
}
|
||||
257
src/main/java/dev/loki/loparkour/storage/StorageSQL.java
Normal file
257
src/main/java/dev/loki/loparkour/storage/StorageSQL.java
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
package dev.loki.loparkour.storage;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.*;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* MySQL storage manager.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class StorageSQL {
|
||||
|
||||
private static Connection connection;
|
||||
|
||||
static {
|
||||
connect();
|
||||
}
|
||||
|
||||
public static void init(String mode) {
|
||||
sendUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS `%s`
|
||||
(
|
||||
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(16),
|
||||
time VARCHAR(16),
|
||||
difficulty VARCHAR(3),
|
||||
score INT
|
||||
)
|
||||
CHARSET = utf8 ENGINE = InnoDB;
|
||||
"""
|
||||
.formatted(getTableName(mode)));
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
try {
|
||||
connection.close();
|
||||
LoParkour.log("Closed connection to MySQL");
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.logging().stack("Error while trying to close connection to SQL database", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
|
||||
try (ResultSet results = sendQuery(
|
||||
"""
|
||||
SELECT * FROM `%s`;
|
||||
"""
|
||||
.formatted(getTableName(mode)))) {
|
||||
|
||||
if (results == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
Map<UUID, Score> scores = new HashMap<>();
|
||||
|
||||
while (results.next()) { // advance row
|
||||
scores.put(UUID.fromString(results.getString("uuid")), new Score(
|
||||
results.getString("name"),
|
||||
results.getString("time"),
|
||||
results.getString("difficulty"),
|
||||
results.getInt("score")));
|
||||
}
|
||||
|
||||
return scores;
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.logging().stack("Error while trying to read SQL data of %s".formatted(mode), ex);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
||||
new HashMap<>(scores).forEach((uuid, score) -> sendUpdate(
|
||||
"""
|
||||
INSERT INTO `%s`
|
||||
(uuid, name, time, difficulty, score)
|
||||
VALUES ('%s', '%s', '%s', '%s', %d)
|
||||
ON DUPLICATE KEY UPDATE name = '%s',
|
||||
time = '%s',
|
||||
difficulty = '%s',
|
||||
score = %d;
|
||||
"""
|
||||
.formatted(getTableName(mode), uuid.toString(), score.name(), score.time(), score.difficulty(), score.score(),
|
||||
score.name(), score.time(), score.difficulty(), score.score())));
|
||||
}
|
||||
|
||||
// returns leaderboard table name
|
||||
private static String getTableName(String mode) {
|
||||
return "%sleaderboard-%s".formatted(Option.SQL_PREFIX, mode);
|
||||
}
|
||||
|
||||
public static void readPlayer(@NotNull ParkourPlayer player) {
|
||||
try (ResultSet results = sendQuery(
|
||||
"""
|
||||
SELECT * FROM `%s` WHERE uuid = '%s';
|
||||
"""
|
||||
.formatted("%soptions".formatted(Option.SQL_PREFIX), player.getUUID()))) {
|
||||
|
||||
if (results == null) {
|
||||
player.setSettings(new HashMap<>());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasNext = results.next(); // move cursor
|
||||
|
||||
if (!hasNext) {
|
||||
player.setSettings(new HashMap<>());
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> settings = Colls.thread(ParkourPlayer.PLAYER_COLUMNS).mapv((key, value) -> {
|
||||
try {
|
||||
return results.getObject(key);
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.logging().stack("Error while trying to read SQL data of %s, option = %s".formatted(player.getName(), key), ex);
|
||||
return null;
|
||||
}
|
||||
}).get();
|
||||
|
||||
player.setSettings(settings);
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.logging().stack("Error while trying to read SQL data of %s".formatted(player.getName()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writePlayer(@NotNull ParkourPlayer player) {
|
||||
DecimalFormat df = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
String schematicDifficulty = df.format(player.schematicDifficulty);
|
||||
|
||||
sendUpdate("""
|
||||
INSERT INTO `%s`
|
||||
(uuid, style, blockLead, useParticles, useSpecial, showFallMsg, showScoreboard,
|
||||
selectedTime, collectedRewards, locale, schematicDifficulty, sound)
|
||||
VALUES ('%s', '%s', %d, %b, %b, %b, %b, %d, '%s', '%s', %s, %b)
|
||||
ON DUPLICATE KEY UPDATE style = '%s',
|
||||
blockLead = %d,
|
||||
useParticles = %b,
|
||||
useSpecial = %b,
|
||||
showFallMsg = %b,
|
||||
showScoreboard = %b,
|
||||
selectedTime = %d,
|
||||
collectedRewards = '%s',
|
||||
locale = '%s',
|
||||
schematicDifficulty = %s,
|
||||
sound = %b;
|
||||
"""
|
||||
.formatted("%soptions".formatted(Option.SQL_PREFIX), player.getUUID(), player.style, player.blockLead,
|
||||
player.particles, player.useSpecialBlocks, player.showFallMessage,
|
||||
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards), player.locale,
|
||||
schematicDifficulty, player.sound,
|
||||
|
||||
player.style, player.blockLead,
|
||||
player.particles, player.useSpecialBlocks, player.showFallMessage,
|
||||
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards), player.locale,
|
||||
schematicDifficulty, player.sound));
|
||||
}
|
||||
|
||||
public static void connect() {
|
||||
try {
|
||||
LoParkour.log("Connecting to MySQL");
|
||||
|
||||
try { // load drivers
|
||||
Class.forName("com.mysql.cj.jdbc.Driver"); // for newer versions
|
||||
} catch (ClassNotFoundException old) {
|
||||
Class.forName("com.mysql.jdbc.Driver"); // for older versions
|
||||
}
|
||||
|
||||
connection = DriverManager.getConnection(("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true" +
|
||||
"&useSSL=false&useUnicode=true&characterEncoding=utf-8" +
|
||||
"&autoReconnect=true&maxReconnects=5").formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB), Option.SQL_USERNAME, Option.SQL_PASSWORD);
|
||||
|
||||
sendUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(Option.SQL_DB));
|
||||
sendUpdate("USE `%s`;".formatted(Option.SQL_DB));
|
||||
|
||||
sendUpdate("CREATE TABLE IF NOT EXISTS `%soptions` (`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, `useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, `showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) ENGINE = InnoDB CHARSET = utf8;".formatted(Option.SQL_PREFIX));
|
||||
|
||||
// v3.0.0
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `time`;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `selectedTime` INT NOT NULL;".formatted(Option.SQL_PREFIX));
|
||||
|
||||
// v3.1.0
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `collectedRewards` MEDIUMTEXT;".formatted(Option.SQL_PREFIX));
|
||||
|
||||
// v3.6.0
|
||||
sendUpdateSuppressed("ALTER TABLE `%s` ADD `locale` VARCHAR(8);".formatted(Option.SQL_PREFIX + "options"));
|
||||
sendUpdateSuppressed("ALTER TABLE `%s` ADD `schematicDifficulty` DOUBLE;".formatted(Option.SQL_PREFIX + "options"));
|
||||
|
||||
// v4.0.0
|
||||
sendUpdateSuppressed("ALTER TABLE `%s` ADD `sound` BOOLEAN;".formatted(Option.SQL_PREFIX + "options"));
|
||||
|
||||
// 5.0.0
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useDifficulty`;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(Option.SQL_PREFIX));
|
||||
|
||||
LoParkour.log("Connected to MySQL");
|
||||
} catch (Exception ex) {
|
||||
LoParkour.logging().stack("Could not connect to MySQL", "check your SQL settings in the config", ex);
|
||||
Bukkit.getPluginManager().disablePlugin(LoParkour.getPlugin()); // disable plugin since data handling without db will go horribly wrong
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateConnection() {
|
||||
try {
|
||||
if (!connection.isValid(10)) {
|
||||
connect();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LoParkour.logging().stack("Error while trying to reconnect to MySQL", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// send query and get result
|
||||
private static ResultSet sendQuery(String sql) {
|
||||
validateConnection();
|
||||
|
||||
try {
|
||||
return connection.prepareStatement(sql).executeQuery();
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.logging().stack("Error while sending query %s".formatted(sql), ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// send update
|
||||
private static void sendUpdate(String sql) {
|
||||
validateConnection();
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.logging().stack("Error while sending query %s".formatted(sql), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// if query throws an error, ignore it
|
||||
private static void sendUpdateSuppressed(String sql) {
|
||||
validateConnection();
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/main/java/dev/loki/loparkour/style/RandomStyle.java
Normal file
28
src/main/java/dev/loki/loparkour/style/RandomStyle.java
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package dev.loki.loparkour.style;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public final class RandomStyle implements Style {
|
||||
|
||||
private final String name;
|
||||
private final List<Material> materials;
|
||||
|
||||
public RandomStyle(String name, List<Material> materials) {
|
||||
this.name = name;
|
||||
this.materials = materials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Material getNext() {
|
||||
return materials.get(ThreadLocalRandom.current().nextInt(materials.size()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
22
src/main/java/dev/loki/loparkour/style/Style.java
Normal file
22
src/main/java/dev/loki/loparkour/style/Style.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package dev.loki.loparkour.style;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
/**
|
||||
* Represents a parkour style.
|
||||
*/
|
||||
public interface Style {
|
||||
|
||||
/**
|
||||
* The material to be used for the next block.
|
||||
* @return A material.
|
||||
*/
|
||||
Material getNext();
|
||||
|
||||
/**
|
||||
* The name of the style.
|
||||
* @return A name.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
}
|
||||
125
src/main/java/dev/loki/loparkour/world/Divider.java
Normal file
125
src/main/java/dev/loki/loparkour/world/Divider.java
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package dev.loki.loparkour.world;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.util.Locations;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* <p>Divides the parkour world in sections, each with an active session.</p>
|
||||
* <p>Iteration 2.</p>
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class Divider {
|
||||
|
||||
/**
|
||||
* Map with all session ids map to the session instances.
|
||||
*/
|
||||
public static final Map<Session, Integer> sections = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Associates a session to a specific section.
|
||||
*
|
||||
* @param session The session.
|
||||
*/
|
||||
public static synchronized Location add(Session session) {
|
||||
// attempts to get the closest available section to the center
|
||||
var missing = IntStream.range(0, sections.size() + 1)
|
||||
.filter(i -> !sections.containsValue(i))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
sections.put(session, missing);
|
||||
|
||||
var location = toLocation(session);
|
||||
|
||||
LoParkour.log("Added session at %s".formatted(Locations.toString(location, true)));
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disassociates a session from a specific section.
|
||||
*
|
||||
* @param session The session.
|
||||
*/
|
||||
public static void remove(Session session) {
|
||||
LoParkour.log("Removed session at %s".formatted(Locations.toString(toLocation(session), true)));
|
||||
|
||||
sections.remove(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param session The session.
|
||||
* @return The location at the center of section n.
|
||||
*/
|
||||
private static Location toLocation(Session session) {
|
||||
int[] xz = spiralAt(sections.get(session));
|
||||
|
||||
return new Location(World.getWorld(),
|
||||
xz[0] * Option.BORDER_SIZE,
|
||||
(Option.MAX_Y + Option.MIN_Y) / 2.0,
|
||||
xz[1] * Option.BORDER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param session The session.
|
||||
* @return Array where the first item is the smallest location and second item is the largest.
|
||||
*/
|
||||
public static Location[] toSelection(Session session) {
|
||||
Location center = toLocation(session);
|
||||
|
||||
// get the min and max locations
|
||||
Location max = center.clone().add(Option.BORDER_SIZE / 2, 0, Option.BORDER_SIZE / 2);
|
||||
Location min = center.clone().subtract(Option.BORDER_SIZE / 2, 0, Option.BORDER_SIZE / 2);
|
||||
|
||||
max.setY(Option.MAX_Y);
|
||||
min.setY(Option.MIN_Y);
|
||||
|
||||
return new Location[]{min, max};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a spiral
|
||||
*
|
||||
* @param n The number of value
|
||||
* @return the coords of this value
|
||||
*/
|
||||
// https://math.stackexchange.com/a/163101
|
||||
private static int[] spiralAt(int n) {
|
||||
if (n < 0) {
|
||||
throw new IllegalArgumentException("Invalid n bound: %d".formatted(n));
|
||||
}
|
||||
|
||||
n++; // one-index
|
||||
int k = (int) Math.ceil((Math.sqrt(n) - 1) / 2);
|
||||
int t = 2 * k + 1;
|
||||
int m = t * t;
|
||||
t--;
|
||||
|
||||
if (n > m - t) {
|
||||
return new int[]{k - (m - n), -k};
|
||||
} else {
|
||||
m -= t;
|
||||
}
|
||||
|
||||
if (n > m - t) {
|
||||
return new int[]{-k, -k + (m - n)};
|
||||
} else {
|
||||
m -= t;
|
||||
}
|
||||
|
||||
if (n > m - t) {
|
||||
return new int[]{-k + (m - n), k};
|
||||
} else {
|
||||
return new int[]{k, k - (m - n - t)};
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/main/java/dev/loki/loparkour/world/World.java
Normal file
126
src/main/java/dev/loki/loparkour/world/World.java
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package dev.loki.loparkour.world;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.efnilite.vilib.util.VoidGenerator;
|
||||
import org.bukkit.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class World {
|
||||
|
||||
private static String name;
|
||||
private static org.bukkit.World world;
|
||||
|
||||
/**
|
||||
* Creates a new world and sets all according settings in it.
|
||||
*/
|
||||
public static void create() {
|
||||
name = Config.CONFIG.getString("world.name");
|
||||
|
||||
var world = Bukkit.getWorld(name);
|
||||
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (world != null) {
|
||||
LoParkour.logging().warn("Crash detected! The parkour world loading twice is not usual behaviour. This only happens after a server crash.");
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("world.delete-on-reload")) {
|
||||
deleteWorld();
|
||||
}
|
||||
|
||||
createWorld();
|
||||
setup();
|
||||
}
|
||||
|
||||
private static void createWorld() {
|
||||
LoParkour.log("Creating Spigot world");
|
||||
|
||||
try {
|
||||
WorldCreator creator = new WorldCreator(name)
|
||||
.generateStructures(false)
|
||||
.type(WorldType.NORMAL)
|
||||
.generator(VoidGenerator.getGenerator()) // to fix No keys in MapLayer etc.
|
||||
.environment(org.bukkit.World.Environment.NORMAL);
|
||||
|
||||
world = Bukkit.createWorld(creator);
|
||||
} catch (Exception ex) {
|
||||
LoParkour.logging().stack("Error while trying to create the parkour world", "delete the parkour world folder and restart the server", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setup() {
|
||||
LoParkour.log("Initializing world rules");
|
||||
|
||||
world.setGameRule(GameRule.DO_FIRE_TICK, false);
|
||||
world.setGameRule(GameRule.DO_MOB_SPAWNING, false);
|
||||
world.setGameRule(GameRule.DO_TILE_DROPS, false);
|
||||
world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
|
||||
world.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
|
||||
world.setGameRule(GameRule.LOG_ADMIN_COMMANDS, false);
|
||||
world.setGameRule(GameRule.KEEP_INVENTORY, true);
|
||||
world.setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
|
||||
|
||||
world.getWorldBorder().setCenter(0, 0);
|
||||
world.getWorldBorder().setSize(10_000_000);
|
||||
world.setDifficulty(Difficulty.PEACEFUL);
|
||||
world.setClearWeatherDuration(1000000);
|
||||
world.setAutoSave(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the world.
|
||||
*/
|
||||
public static void delete() {
|
||||
if (!Config.CONFIG.getBoolean("world.delete-on-reload") || !Config.CONFIG.getBoolean("joining")) {
|
||||
return;
|
||||
}
|
||||
LoParkour.log("Deleting world");
|
||||
|
||||
getWorld().getPlayers().forEach(player -> player.kickPlayer("Server is restarting"));
|
||||
|
||||
deleteWorld();
|
||||
}
|
||||
|
||||
private static void deleteWorld() {
|
||||
LoParkour.log("Deleting Spigot world");
|
||||
|
||||
File file = new File(name);
|
||||
|
||||
// world has already been deleted
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bukkit.unloadWorld(name, false);
|
||||
|
||||
try (Stream<Path> files = Files.walk(file.toPath())) {
|
||||
files.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch (Exception ex) {
|
||||
LoParkour.logging().stack("Error while trying to delete the parkour world", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name of the parkour world.
|
||||
*/
|
||||
public static String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Bukkit world wherein LoParkour is currently active.
|
||||
*/
|
||||
public static org.bukkit.World getWorld() {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
352
src/main/resources/config.yml
Normal file
352
src/main/resources/config.yml
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
#===============================================================================#
|
||||
# #
|
||||
# 88 88888888ba #
|
||||
# 88 88 "8b #
|
||||
# 88 88 ,8P #
|
||||
# 88 88aaaaaa8P' #
|
||||
# 88 88""""""' #
|
||||
# 88 88 #
|
||||
# 88 88 #
|
||||
# 88 88 #
|
||||
# #
|
||||
#===============================================================================#
|
||||
|
||||
#===============================================================================#
|
||||
# #
|
||||
# (c) loki 2026 #
|
||||
# #
|
||||
#===============================================================================#
|
||||
|
||||
# -= Debugging =-
|
||||
debug: false
|
||||
|
||||
# -= Enable joining =-
|
||||
# Whether players can join the parkour.
|
||||
# When joining is disabled, the IP instance on this server will only read data from leaderboard databases.
|
||||
# Set joining to false if you want to create a cross-server leaderboard on this server.
|
||||
# true = players can join, false = players can't join.
|
||||
joining: true
|
||||
|
||||
# -= Proxy options =-
|
||||
# Change the proxy settings. Applies to Velocity as well.
|
||||
# This also applies to servers who only want to run this plugin.
|
||||
bungeecord:
|
||||
|
||||
# -= Proxy enabled =-
|
||||
# Enabling this will make players who join the server automatically join the parkour.
|
||||
# You can enable this even without BungeeCord to make this a parkour-only server.
|
||||
# True = players will automatically join the parkour on join, false = players have to use the menu/parkour command.
|
||||
enabled: false
|
||||
|
||||
# -= Proxy return server =-
|
||||
# The server the players will be returned to after they leave.
|
||||
return_server: 'Lobby'
|
||||
|
||||
# -= Go back enabled =-
|
||||
# Whether players should be teleported to a specific location after leaving.
|
||||
# True = players get teleported to a specific location when they leave, false = they go to their location when they joined.
|
||||
go-back-enabled: false
|
||||
|
||||
# -= Go back location =-
|
||||
# The place players will go back to when they leave
|
||||
# Only applies if proxy mode is disabled and the go-back-enabled option is true.
|
||||
# Syntax: '(x,y,z,worldName)'
|
||||
go-back: '(0,100,0,world)'
|
||||
|
||||
# -= Go back axes =-
|
||||
# Sets the pitch and yaw of the go-back location.
|
||||
# The first argument is the pitch, second yaw.
|
||||
go-back-axes: '0,0'
|
||||
|
||||
# -= Storage update interval =-
|
||||
# The amount of seconds between every disk/database storage update.
|
||||
# In this update, the in-memory scores will be written to storage.
|
||||
# A lower number might reduce the chance of losing data on server crashes,
|
||||
# but will increase the amount of requests to the disk/database.
|
||||
# (requires a restart, default = 300)
|
||||
storage-update-interval: 300
|
||||
|
||||
# -= MySQL =-
|
||||
# The options for using MySQL as storage.
|
||||
sql:
|
||||
|
||||
# -= MySQL enabled =-
|
||||
# Using databases makes you able to sync player data across multiple servers.
|
||||
# True = data will be stored in a database, false = data will be stored locally.
|
||||
enabled: false
|
||||
|
||||
# -= MySQL settings =-
|
||||
url: 'localhost'
|
||||
port: 3306
|
||||
username: 'efnilite'
|
||||
password: 'super secret password'
|
||||
database: 'ip'
|
||||
|
||||
# -= MySQL prefix =-
|
||||
# The prefix given to any table names
|
||||
# The default is ''. Example: 'ip-' -> this would make the table 'ip-players'
|
||||
prefix: ''
|
||||
|
||||
# -= World options =-
|
||||
world:
|
||||
|
||||
# -= World name =-
|
||||
# The world in which parkour will take place
|
||||
name: 'witp'
|
||||
|
||||
# -= Delete world on reload =-
|
||||
# Deletes the world on reload/restart of the server.
|
||||
# This makes sure there are no ghost blocks left when a server crash occurs.
|
||||
# Disable this if you want to have a custom map.
|
||||
delete-on-reload: true
|
||||
|
||||
# -= World fall back =-
|
||||
# The world players will be teleported to if they accidentally join the Parkour world without joining parkour
|
||||
fall-back: 'world'
|
||||
|
||||
# -= General options =-
|
||||
options:
|
||||
|
||||
# -= Leads options =-
|
||||
leads:
|
||||
|
||||
# -= Leads amounts =-
|
||||
# The lead amounts a player can select in their options menu.
|
||||
# Values also need to be above 1 and below 64.
|
||||
amount:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 10
|
||||
|
||||
# -= Time options =-
|
||||
time:
|
||||
|
||||
# -= Time format =-
|
||||
# Choose between:
|
||||
# 24 - 24 hour clock. (00:00, 12:00, 23:59)
|
||||
# 12 - 12 hour clock. (12:00 AM, 12:00 PM, 11:59 PM)
|
||||
format: 24
|
||||
|
||||
# -= Score time format =-
|
||||
# H = hours, m = minutes, s = seconds, S = milliseconds
|
||||
score-format: "mm:ss:SSS"
|
||||
|
||||
# -= Inventory handling =-
|
||||
# Toggles the inventory handling system.
|
||||
# True = players' inventories get saved and cleared when they join the parkour and when they leave they get their items back
|
||||
# False = players keep their inventories when they join the parkour
|
||||
inventory-handling: true
|
||||
|
||||
# -= Inventory saving =-
|
||||
# Logs inventories of the players that join to the inventories/ folder.
|
||||
# It is highly recommended to keep this enabled. Server crashes or errors while leaving might cause
|
||||
# inventories of players to be lost if this is set to false!
|
||||
# Use /ip recoverinventory to recover inventories of players who joined the parkour and who lost their inventories in a crash, etc.
|
||||
inventory-saving: true
|
||||
|
||||
# -= Alternate inventory saving command =-
|
||||
# If you don't trust this plugin to save inventories, you can select another plugin to execute a command
|
||||
# to save players' inventories.
|
||||
# Example: /inventory save %player%
|
||||
# This command will be executed by console.
|
||||
alt-inventory-saving-command: ''
|
||||
|
||||
# -= Disables inventory blocks =-
|
||||
# Disables the ability for a player to open blocks with an inventory.
|
||||
disable-inventory-blocks: false
|
||||
|
||||
# -= Permissions options =-
|
||||
# These permissions are used to determine which things the player can change.
|
||||
# Find them at https://efnilite.dev/projects/ip/wiki
|
||||
permissions:
|
||||
|
||||
# -= Permissions enabled =-
|
||||
# If permission handling is enabled.
|
||||
# True = players need a permission to join, view the leaderboard, etc., false = players can do and view anything
|
||||
# from this plugin besides admin commands.
|
||||
enabled: false
|
||||
|
||||
# -= Per style permissions =-
|
||||
# Gives every style a permission. The permission is the name of the style.
|
||||
# Syntax: ip.settings.styles.<style> - example: ip.settings.styles.red
|
||||
# Useful for servers with ranks.
|
||||
per-style: false
|
||||
|
||||
# -= Focus mode options =-
|
||||
focus-mode:
|
||||
|
||||
# -= Focus mode enabled =-
|
||||
# If focus mode is enabled players can't use commands while in parkour.
|
||||
# Note: this might not work on all commands because Minecraft.
|
||||
enabled: false
|
||||
|
||||
# -=
|
||||
# A whitelist for commands which ARE allowed during focus mode (if focus mode is enabled).
|
||||
# If you want to disable the default whitelisted commands you can remove every item in the list below.
|
||||
whitelist:
|
||||
- 'ip'
|
||||
- 'parkour'
|
||||
- 'msg'
|
||||
- 'r'
|
||||
- 'w'
|
||||
- 'whisper'
|
||||
- 'reply'
|
||||
- 'message'
|
||||
|
||||
# -= Styles options =-
|
||||
styles:
|
||||
|
||||
# -= Styles list =-
|
||||
# The list of all available styles, which you can add to if you want.
|
||||
# For changes, you have to restart or reload your server.
|
||||
list:
|
||||
red: [red_wool, red_stained_glass, red_concrete, red_terracotta]
|
||||
blue: [blue_wool, blue_stained_glass, blue_concrete, blue_terracotta]
|
||||
light blue: [light_blue_wool, light_blue_stained_glass, light_blue_concrete, light_blue_terracotta]
|
||||
green: [green_wool, green_stained_glass, green_concrete, green_terracotta]
|
||||
lime: [lime_wool, lime_stained_glass, lime_concrete, lime_terracotta]
|
||||
cyan: [cyan_wool, cyan_stained_glass, cyan_concrete, cyan_terracotta]
|
||||
purple: [purple_wool, purple_stained_glass, purple_concrete, purple_terracotta]
|
||||
yellow: [yellow_wool, yellow_stained_glass, yellow_concrete, yellow_terracotta]
|
||||
gray: [gray_wool, gray_stained_glass, gray_concrete, gray_terracotta]
|
||||
end: [end_stone, end_stone_bricks, purpur_block, purpur_pillar]
|
||||
nether: [netherrack, nether_bricks, red_nether_bricks, chiseled_nether_bricks, nether_wart_block, nether_gold_ore, netherite_block, basalt]
|
||||
quartz: [smooth_quartz, chiseled_quartz_block, quartz_block, quartz_bricks, quartz_pillar]
|
||||
ocean: [prismarine, prismarine_bricks, dark_prismarine, sea_lantern]
|
||||
redstone: [observer, dispenser, redstone_block, piston, sticky_piston, redstone_lamp, target]
|
||||
overworld: [grass_block, stone, cobblestone, oak_log]
|
||||
ores: [diamond_ore, gold_ore, iron_ore, coal_ore, nether_gold_ore, lapis_ore, redstone_ore, emerald_ore, nether_quartz_ore]
|
||||
wood: [oak_planks, spruce_planks, jungle_planks, acacia_planks, birch_planks, dark_oak_planks, oak_log, spruce_log, jungle_log, birch_log, acacia_log, dark_oak_log]
|
||||
rainbow: [red_wool, blue_wool, orange_wool, magenta_wool, lime_wool, purple_wool, yellow_concrete, orange_concrete, lime_concrete, light_blue_concrete, yellow_stained_glass, white_stained_glass, magenta_stained_glass, light_blue_stained_glass]
|
||||
glow: [glowstone, jack_o_lantern, beacon, sea_lantern, crying_obsidian, shroomlight]
|
||||
|
||||
# -= Scoring options =-
|
||||
scoring:
|
||||
|
||||
# -= All points =-
|
||||
# This will give the player all possible points.
|
||||
# If a player skips a block jump, it will still give the points for that block.
|
||||
all-points: false
|
||||
|
||||
# -= Rewards use total score =-
|
||||
# Whether interval rewards should use the total score of a session or not.
|
||||
# The total score doesn't reset when a player falls.
|
||||
rewards-use-total-score: false
|
||||
|
||||
# -= Particle options =-
|
||||
particles:
|
||||
|
||||
# -= Particle shape =-
|
||||
# The shape of the particle that will appear when a new block generates.
|
||||
# Possible options: dot, box, circle
|
||||
particle-shape: 'box'
|
||||
|
||||
# -= Particle type =-
|
||||
# The type of particle that will appear when a new block generates.
|
||||
# You can find all types at https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Particle.html
|
||||
particle-type: 'SPELL_INSTANT'
|
||||
|
||||
# -= Sound type =-
|
||||
# The sound that will play when a new block generates.
|
||||
# You can find all sounds at https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html
|
||||
sound-type: 'BLOCK_NOTE_BLOCK_GUITAR'
|
||||
|
||||
# -= Sound pitch =-
|
||||
# The pitch of the sound that will play when a new block generates.
|
||||
sound-pitch: 3
|
||||
|
||||
# -= Sound volume =-
|
||||
sound-volume: 2
|
||||
|
||||
# -= Default values =-
|
||||
# This is a list of all options and defaults.
|
||||
default-values:
|
||||
main:
|
||||
enabled: true
|
||||
|
||||
quit:
|
||||
enabled: true
|
||||
|
||||
play:
|
||||
enabled: true
|
||||
|
||||
single:
|
||||
enabled: true
|
||||
|
||||
spectator:
|
||||
enabled: true
|
||||
|
||||
community:
|
||||
enabled: true
|
||||
|
||||
leaderboards:
|
||||
enabled: true
|
||||
|
||||
settings:
|
||||
enabled: true
|
||||
|
||||
lang:
|
||||
enabled: true
|
||||
default: "en"
|
||||
|
||||
chat:
|
||||
enabled: true
|
||||
|
||||
parkour_settings:
|
||||
|
||||
item:
|
||||
enabled: true
|
||||
|
||||
items:
|
||||
|
||||
styles:
|
||||
enabled: true
|
||||
default: "red"
|
||||
|
||||
leads:
|
||||
enabled: true
|
||||
default: 4
|
||||
|
||||
time:
|
||||
enabled: true
|
||||
default: 6000
|
||||
|
||||
schematics:
|
||||
enabled: true
|
||||
default: 0.6
|
||||
|
||||
scoreboard:
|
||||
enabled: true
|
||||
default: true
|
||||
|
||||
fall_message:
|
||||
enabled: true
|
||||
default: true
|
||||
|
||||
particles:
|
||||
enabled: true
|
||||
default: true
|
||||
|
||||
sound:
|
||||
enabled: true
|
||||
default: true
|
||||
|
||||
special_blocks:
|
||||
enabled: true
|
||||
default: true
|
||||
|
||||
lobby:
|
||||
enabled: true
|
||||
|
||||
visibility:
|
||||
enabled: true
|
||||
|
||||
player_management:
|
||||
enabled: true
|
||||
86
src/main/resources/generation.yml
Normal file
86
src/main/resources/generation.yml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# -= Parkour generation settings =-
|
||||
# Settings for parkour generation
|
||||
generation:
|
||||
|
||||
# -= Jump type =-
|
||||
# The chance of each type of jump. Can be above 100%.
|
||||
type:
|
||||
normal: 85
|
||||
special: 10
|
||||
schematic: 5
|
||||
|
||||
# -= Normal jump chances =-
|
||||
normal:
|
||||
# -= Distance chances =-
|
||||
# The chances of the different distances in %. Can be above 100%.
|
||||
distance:
|
||||
1: 10
|
||||
2: 55
|
||||
3: 34
|
||||
4: 1
|
||||
|
||||
# -= Height chances =-
|
||||
# The chances of the different heights a jump can have in %. Can be above 100%.
|
||||
height:
|
||||
1: 20
|
||||
0: 65
|
||||
-1: 10
|
||||
-2: 5
|
||||
|
||||
# -= Special jump chances =-
|
||||
special:
|
||||
ice: 50
|
||||
slab: 30
|
||||
pane: 10
|
||||
fence: 10
|
||||
|
||||
# -= Jump settings =-
|
||||
settings:
|
||||
|
||||
# -= Min jump y value =-
|
||||
# The minimal y value for parkour to be generated at.
|
||||
# If you are using a custom world, you should set this to a value where the parkour won't intersect with the world.
|
||||
min-y: 100
|
||||
|
||||
# -= Max jump y value =-
|
||||
# The maximum y value for parkour to be generated at.
|
||||
# If you are using a custom world, you should set this to a value where the parkour won't intersect with the world.
|
||||
max-y: 200
|
||||
|
||||
# -= Advanced settings =-
|
||||
# Don't modify these if you don't know what you're doing!
|
||||
advanced:
|
||||
|
||||
# -= Spawn island options =-
|
||||
# Options for the spawn island
|
||||
island:
|
||||
spawn:
|
||||
|
||||
# -= Player block =-
|
||||
# The material of the block that the player will spawn on.
|
||||
# This block will be set to air once the schematic has been pasted.
|
||||
player-block: diamond_block
|
||||
|
||||
# -= Spawn yaw/pitch =-
|
||||
yaw: -90
|
||||
pitch: 0
|
||||
|
||||
parkour:
|
||||
|
||||
# -= Begin block =-
|
||||
# The block from which the parkour begins
|
||||
# This block will be set to air once the schematic has been pasted
|
||||
begin-block: emerald_block
|
||||
|
||||
# -= Directional heading =-
|
||||
# The directional heading of the parkour
|
||||
# Possible options: north, south, west and east.
|
||||
heading: east
|
||||
|
||||
# -= Border size =-
|
||||
# The diameter of the playing area of a person in the single mode.
|
||||
border-size: 10000
|
||||
|
||||
# -= Schematic cooldown =-
|
||||
# The amount of blocks that provide a cooldown for the schematics.
|
||||
schematic-cooldown: 20
|
||||
260
src/main/resources/locales/en.yml
Normal file
260
src/main/resources/locales/en.yml
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
# English (English, en)
|
||||
# By loki
|
||||
# Copy this file and rename it to add your own language!
|
||||
# Materials have to be from: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html
|
||||
# Learn more about colouring: https://docs.adventure.kyori.net/minimessage/format.html#format
|
||||
|
||||
name: "English"
|
||||
|
||||
main:
|
||||
name: "&#FFFFFFParkour"
|
||||
|
||||
play:
|
||||
name: "&#FFFFFFPlay"
|
||||
|
||||
item:
|
||||
material: "sugar_cane"
|
||||
name: "�D4FF<bold>Play"
|
||||
lore: ""
|
||||
|
||||
single:
|
||||
name: "&#FFFFFFSingle"
|
||||
|
||||
item:
|
||||
material: "ender_pearl"
|
||||
name: "�D4FF<bold>Single-player"
|
||||
lore: ""
|
||||
|
||||
default:
|
||||
material: "barrel"
|
||||
name: "�D4FF<bold>Default"
|
||||
lore: "񢩈The default gamemode."
|
||||
|
||||
spectator:
|
||||
name: "&#FFFFFFSpectator"
|
||||
|
||||
join: "�D4FFYou are now a spectator. You can use \"/parkour\" to leave/change your gamemode."
|
||||
action_bar: "�D4FFYou are a spectator &#A0A0A0• �D4FFUse \"/parkour\" to leave."
|
||||
|
||||
other_join: "<dark_green><bold>+ <reset>&#A0A0A0%s started spectating you"
|
||||
other_leave: "&#FF4444<bold>- <reset>&#A0A0A0%s stopped spectating you"
|
||||
|
||||
item:
|
||||
material: "glass"
|
||||
name: "�D4FF<bold>Spectator"
|
||||
lore: ""
|
||||
|
||||
head:
|
||||
material: ""
|
||||
name: "�D4FF<bold>%s"
|
||||
lore: ""
|
||||
|
||||
community:
|
||||
name: "&#FFFFFFCommunity"
|
||||
|
||||
item:
|
||||
material: "oak_boat"
|
||||
name: "�D4FF<bold>Community"
|
||||
lore: ""
|
||||
|
||||
leaderboards:
|
||||
name: "&#FFFFFFLeaderboards"
|
||||
|
||||
item:
|
||||
material: "gold_nugget"
|
||||
name: "�D4FF<bold>Leaderboards"
|
||||
lore: ""
|
||||
|
||||
sort:
|
||||
material: "bookshelf"
|
||||
name: "�D4FF<bold>Sorted by %s"
|
||||
lore: ""
|
||||
values: ["score", "time", "difficulty"]
|
||||
|
||||
head:
|
||||
material: ""
|
||||
name: "�D4FF<bold>#%r - %p"
|
||||
lore: "�D4FFScore: &#A0A0A0%s||�D4FFTime: &#A0A0A0%t||�D4FFDifficulty: &#A0A0A0%d񢩈/1.0"
|
||||
|
||||
settings:
|
||||
name: "&#FFFFFFSettings"
|
||||
|
||||
item:
|
||||
material: "comparator"
|
||||
name: "�D4FF<bold>Settings"
|
||||
lore: ""
|
||||
|
||||
lang:
|
||||
name: "&#FFFFFFLanguage"
|
||||
|
||||
item:
|
||||
material: "writable_book"
|
||||
name: "�D4FF<bold>Language"
|
||||
lore: "񢩈Currently: �D4FF<bold>%s"
|
||||
|
||||
chat:
|
||||
material: "crossbow"
|
||||
name: "�D4FF<bold>Chat"
|
||||
lore: "񢩈Currently: �D4FF<bold>%s||񢩈Select who you can chat with"
|
||||
values: ["lobby only", "players only", "public"]
|
||||
|
||||
formats:
|
||||
lobby: "�D4FF(Lobby) �D4FF%s 񢩈> &#A0A0A0%s"
|
||||
players: "�D4FF(Players) �D4FF%s 񢩈> &#A0A0A0%s"
|
||||
|
||||
parkour_settings:
|
||||
|
||||
enabled: "�D4FF<bold>✔"
|
||||
disabled: "&#FF4444<bold>✘"
|
||||
|
||||
item:
|
||||
material: "redstone"
|
||||
name: "�D4FF<bold>Parkour Settings"
|
||||
lore: "񢩈Change the way your parkour appears."
|
||||
|
||||
items:
|
||||
|
||||
no_change:
|
||||
material: "barrier"
|
||||
name: "<dark_red><bold>You cant change this right now!"
|
||||
lore: "&#A0A0A0You have a score above 0,||&#A0A0A0which means you cant change this setting."
|
||||
|
||||
styles:
|
||||
name: "&#FFFFFFStyles"
|
||||
|
||||
style_item:
|
||||
material: ""
|
||||
name: "&#FFFFFF<bold>%s"
|
||||
lore: "�D4FFCategory: &#A0A0A0%s"
|
||||
|
||||
item:
|
||||
material: "crafting_table"
|
||||
name: "�D4FF<bold>Style"
|
||||
lore: "�D4FFCurrently: &#A0A0A0%s||&#A0A0A0Choose which blocks will be used."
|
||||
|
||||
leads:
|
||||
material: "feather"
|
||||
name: "�D4FF<bold>Lead"
|
||||
lore: "�D4FFCurrently: &#A0A0A0%s blocks||&#A0A0A0How many blocks will||&#A0A0A0be generated in front of you."
|
||||
|
||||
time:
|
||||
material: "clock"
|
||||
name: "�D4FF<bold>Time"
|
||||
lore: "�D4FFCurrently: &#A0A0A0%s||&#A0A0A0Your current visual time."
|
||||
|
||||
schematics:
|
||||
material: ""
|
||||
name: "�D4FF<bold>Schematic difficulty"
|
||||
lore: "�D4FFCurrently: &#A0A0A0%s||&#A0A0A0The difficulty of schematics"
|
||||
values: ["&#FF4444off", "�D4FFeasy", "<yellow>medium", "�D4FFhard", "<dark_red>very hard"]
|
||||
|
||||
scoreboard:
|
||||
material: ""
|
||||
name: "Show scoreboard"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled shows the scoreboard."
|
||||
|
||||
fall_message:
|
||||
item:
|
||||
material: ""
|
||||
name: "Show fall message"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled shows a message when you fall||&#A0A0A0with extra info."
|
||||
|
||||
divider: "񢩈<strikethrough>-----------------------------------"
|
||||
score: "�D4FF<bold>Score <reset>&#FFFFFF%s"
|
||||
time: "�D4FF<bold>Time <reset>&#FFFFFF%s"
|
||||
high_score: "�D4FF<bold>High-score <reset>&#FFFFFF%s"
|
||||
|
||||
formats:
|
||||
miss: "&#A0A0A0You missed your high score by %s points!"
|
||||
beat: "&#A0A0A0You beat your high score by %s points!"
|
||||
tied: "&#A0A0A0You tied your high score!"
|
||||
|
||||
particles:
|
||||
material: ""
|
||||
name: "Show particles"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled, every generated block||&#A0A0A0will show particles."
|
||||
|
||||
sound:
|
||||
material: ""
|
||||
name: "Play sounds"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled, every generated block||&#A0A0A0will play a sound."
|
||||
|
||||
special_blocks:
|
||||
material: ""
|
||||
name: "Use special blocks"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled, uses special blocks like ice and slabs."
|
||||
|
||||
score_difficulty:
|
||||
material: ""
|
||||
name: "Score difficulty"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0When enabled, the higher score you have,||&#A0A0A0the more difficult it gets."
|
||||
|
||||
lobby:
|
||||
name: "&#FFFFFFLobby"
|
||||
|
||||
other_join: '<dark_green><bold>+ <reset>&#A0A0A0%s joined your lobby'
|
||||
other_leave: '&#FF4444<bold>- <reset>&#A0A0A0%s left your lobby'
|
||||
|
||||
item:
|
||||
material: "iron_ingot"
|
||||
name: "�D4FF<bold>Your Lobby"
|
||||
lore: ""
|
||||
|
||||
visibility:
|
||||
material: "firework_rocket"
|
||||
name: "�D4FF<bold>Visibility"
|
||||
lore: "񢩈Currently: �D4FF<bold>%s||񢩈Select who can join your lobby."
|
||||
values: ["private", "invite-only", "public"]
|
||||
|
||||
player_management:
|
||||
name: "&#FFFFFFManage players"
|
||||
|
||||
kicked: "&#A0A0A0You've been kicked from your lobby by the lobby owner."
|
||||
advice: "&#A0A0A0Set your lobby visibility to invite-only or private to avoid people randomly joining."
|
||||
muted: "&#A0A0A0You've been muted by the lobby owner."
|
||||
unmuted: "&#A0A0A0You've been unmuted by the lobby owner."
|
||||
|
||||
item:
|
||||
material: "cake"
|
||||
name: "�D4FF<bold>Manage Players"
|
||||
lore: "񢩈Kick or mute players here."
|
||||
|
||||
head:
|
||||
material: ""
|
||||
name: "�D4FF<bold>%s"
|
||||
lore: ""
|
||||
top: "񢩈Muted in lobby||"
|
||||
bottom: "�D4FFLeft click񢩈 to kick from lobby||�D4FFRight click񢩈 to (un)mute in lobby"
|
||||
|
||||
scoreboard:
|
||||
|
||||
title: "�D4FF<bold>LoParkour"
|
||||
|
||||
lines:
|
||||
- "<blue>"
|
||||
- "�D4FFScore &#FFFFFF%score%"
|
||||
- "�D4FFTime &#FFFFFF%time%"
|
||||
- "�D4FFHighscore &#FFFFFF%high_score%"
|
||||
- "&#FF4444"
|
||||
- "�D4FF<bold>server.ip"
|
||||
|
||||
other:
|
||||
|
||||
no_do: "&#A0A0A0You can't do this right now."
|
||||
no_teleporting: "&#FFFFFFYou can't enter the parkour world by teleporting!"
|
||||
customize: "&#A0A0A0Customize your parkour experience by using \"/parkour\""
|
||||
|
||||
quit:
|
||||
material: "barrier"
|
||||
name: "�D4FF<bold>Quit"
|
||||
lore: ""
|
||||
|
||||
close:
|
||||
material: "arrow"
|
||||
name: "�D4FF<bold>Close"
|
||||
lore: ""
|
||||
|
||||
iep:
|
||||
material: "elytra"
|
||||
name: "�D4FF<bold>Flying"
|
||||
lore: "&#A0A0A0Parkour with elytra!"
|
||||
51
src/main/resources/locales/ru.yml
Normal file
51
src/main/resources/locales/ru.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
menu:
|
||||
main:
|
||||
title: "&#FF6464Главное меню"
|
||||
play: "�D4FFИграть"
|
||||
community: "&#FFD700Сообщество"
|
||||
settings: "&#A0A0A0Настройки"
|
||||
lobby: "ZEE90Лобби"
|
||||
quit: "&#FF4444Выход"
|
||||
|
||||
play:
|
||||
title: "�D4FFВыбор режима"
|
||||
single: "�FF00Одиночный"
|
||||
spectator: "&#FFFF00Наблюдатель"
|
||||
|
||||
settings:
|
||||
title: "&#A0A0A0Настройки"
|
||||
language: "&#FFFFFFЯзык"
|
||||
parkour: "�D4FFПаркур"
|
||||
chat: "ZEE90Чат"
|
||||
|
||||
parkour_settings:
|
||||
title: "�D4FFНастройки паркура"
|
||||
style: "&#FF6464Стиль блоков"
|
||||
leads: "&#FFD700Количество блоков"
|
||||
time: "&#A0A0A0Время"
|
||||
schematics: "�FFFFСхематики"
|
||||
scoreboard: "ZEE90Табло"
|
||||
fall_message: "&#FF4444Сообщение о падении"
|
||||
particles: "&#FFAAFFЧастицы"
|
||||
sound: "&#FFFFAAЗвук"
|
||||
special_blocks: "�D4FFСпециальные блоки"
|
||||
|
||||
community:
|
||||
title: "&#FFD700Сообщество"
|
||||
leaderboards: "�D4FFТаблица лидеров"
|
||||
|
||||
lobby:
|
||||
title: "ZEE90Лобби"
|
||||
visibility: "&#FFFFFFВидимость игроков"
|
||||
management: "&#A0A0A0Управление"
|
||||
|
||||
messages:
|
||||
join: "�D4FFВы присоединились к паркуру!"
|
||||
leave: "&#FF6464Вы покинули паркур"
|
||||
fall: "&#FF4444Вы упали! Счет: %score%"
|
||||
score: "&#FFD700Новый рекорд: %score%!"
|
||||
no_permission: "&#FF4444У вас нет прав!"
|
||||
|
||||
commands:
|
||||
help: "�D4FF/loparkour &#FFFFFF- Главное меню"
|
||||
reload: "�D4FFКонфигурация перезагружена!"
|
||||
12
src/main/resources/plugin.yml
Normal file
12
src/main/resources/plugin.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
name: 'LoParkour'
|
||||
description: 'Advanced infinitely generating parkour plugin.'
|
||||
author: loki
|
||||
version: 1.1.0
|
||||
api-version: 1.16
|
||||
main: dev.loki.loparkour.LoParkour
|
||||
softdepend: [floodgate, Vault, PlaceholderAPI, Multiverse-Core, VoidGen]
|
||||
commands:
|
||||
loparkour:
|
||||
aliases: [parkour, lp]
|
||||
usage: /<command> [arguments]
|
||||
description: Main command for LoParkour
|
||||
54
src/main/resources/rewards-v2.yml
Normal file
54
src/main/resources/rewards-v2.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# ----------- Rewards v2 -----------
|
||||
# Each command needs a separate line, as you can see below.
|
||||
# You can copy the format below and change the score/commands to add a reward.
|
||||
# Reward commands are executed by the console. Don't add "/"!
|
||||
# Scores of 0 do not work.
|
||||
# If no mode is specified, "default" will be used.
|
||||
|
||||
# Custom reward syntaxes:
|
||||
# %player% - the player's name.
|
||||
# test %player% - execute a console command. Don't add "/"!
|
||||
# leave:test - executes a command on leave.
|
||||
# send:<bold>test - sends a message.
|
||||
# vault:-87 - deposits a specific amount of money (Vault required)
|
||||
# default:test - mode-specific rewards. Mode names are "default", or for IP+ modes the same as the name used in the IP+ config.
|
||||
#
|
||||
# Learn more about colouring: https://docs.advntr.dev/minimessage/format.html
|
||||
|
||||
# -= Rewards enabled =-
|
||||
# Are rewards enabled?
|
||||
enabled: false
|
||||
|
||||
# -= Score rewards =-
|
||||
# Rewards when a player reaches a specific score
|
||||
score-rewards:
|
||||
420:
|
||||
- "send:good job :)"
|
||||
- "speed:send:You're like Lightning McQueen"
|
||||
|
||||
# -= Interval rewards =-
|
||||
# Rewards which will be given at certain intervals.
|
||||
# An interval of 10 means a player will get a reward at 10, 20, 30, etc.
|
||||
interval-rewards:
|
||||
10:
|
||||
- "witpreward10"
|
||||
- "send:<green>Good job!"
|
||||
|
||||
# -= One time rewards =-
|
||||
# Rewards which players will receive just once.
|
||||
# These are based on the score required.
|
||||
# Changing the commands when a player already has achieved this score will not give it to them again.
|
||||
# Warning: one-time rewards only work based on score. This might cause issues with per-mode rewards not being awarded.
|
||||
one-time-rewards:
|
||||
1:
|
||||
- "send:You made your first jump!"
|
||||
- "team_survival:send:<gray>Be sure to invite your friends!"
|
||||
- "duels:send:<gray>Good luck!"
|
||||
2023:
|
||||
- "leave:currentyear"
|
||||
- "vault:100"
|
||||
2024:
|
||||
- "send:<reset>"
|
||||
- "send:<red><bold>Time travelling is illegal!"
|
||||
- "send:<reset>"
|
||||
- "vault:-100"
|
||||
BIN
src/main/resources/schematics/parkour-0442a82a
Normal file
BIN
src/main/resources/schematics/parkour-0442a82a
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-11a03fac
Normal file
BIN
src/main/resources/schematics/parkour-11a03fac
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-194fdb81
Normal file
BIN
src/main/resources/schematics/parkour-194fdb81
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-27f41406
Normal file
BIN
src/main/resources/schematics/parkour-27f41406
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-2b36717a
Normal file
BIN
src/main/resources/schematics/parkour-2b36717a
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-2ff722f3
Normal file
BIN
src/main/resources/schematics/parkour-2ff722f3
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-3087330f
Normal file
BIN
src/main/resources/schematics/parkour-3087330f
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-3513c613
Normal file
BIN
src/main/resources/schematics/parkour-3513c613
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-3f66b71e
Normal file
BIN
src/main/resources/schematics/parkour-3f66b71e
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-3fda7be9
Normal file
BIN
src/main/resources/schematics/parkour-3fda7be9
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-47770924
Normal file
BIN
src/main/resources/schematics/parkour-47770924
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-4941bec6
Normal file
BIN
src/main/resources/schematics/parkour-4941bec6
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-4a67b679
Normal file
BIN
src/main/resources/schematics/parkour-4a67b679
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-564cf053
Normal file
BIN
src/main/resources/schematics/parkour-564cf053
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-5b7fdc6c
Normal file
BIN
src/main/resources/schematics/parkour-5b7fdc6c
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-7006faf0
Normal file
BIN
src/main/resources/schematics/parkour-7006faf0
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-7ba9f27e
Normal file
BIN
src/main/resources/schematics/parkour-7ba9f27e
Normal file
Binary file not shown.
BIN
src/main/resources/schematics/parkour-7de341cf
Normal file
BIN
src/main/resources/schematics/parkour-7de341cf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue