Compare commits

..
78 Commits
Author SHA1 Message Date
VoidsipandJosh Roy f8724e26ef Honor the configured Discord private-message format (#6644)
### Information

This PR fixes #6641.

### Details

**Proposed fix:**

Read the private-message format from `messages.pms-to-discord`, matching
the shipped config. The settings loader currently reads
`messages.private-chat` instead, so a custom format is ignored and the
default `[SocialSpy]` text is used.

Added settings tests that load real YAML files and check the custom
format, default fallback and reload, including placeholders and
apostrophes. The Discord module now includes the core project on its
test classpath.

**Environments tested:**

OS: Windows
Java version: Temurin 25.0.4.1+1

- [ ] Most recent Paper version
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8

The full Gradle build passes, including the new settings tests and
Checkstyle. Live Paper/Discord relay testing was not performed.

**Demonstration:**

The regression loads this configuration:

```yaml
messages:
  pms-to-discord: "[Privado] {sender-displayname} -> {receiver-displayname}: {message}"
```

Before the fix, the actual settings loader produces `[SocialSpy] sender
-> receiver: Hello`. After the fix, it produces `[Privado] Sender ->
Receiver: Hello`. The missing-key control keeps the existing default.

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-11-03 16:22:56 +00:00
minoneer e70bdb817c Fix IndexOutOfBoundsException in AlternativeCommandsHandler.getAlternative (#6609)
<!--

EssentialsX bug fix submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a bug fix, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original bug
report. If there isn't an existing bug report, we recommend opening a
new
detailed bug report BEFORE opening your PR to fix it, else your PR may
be
      delayed or rejected without warning.
      
      You can open a new bug report by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose 

2.  When linking logs or config files, do not attach them to the post!
Copy and paste any logs into https://gist.github.com/, then paste a
link to them in the relevant parts of the template. Do not use Hastebin
      or Pastebin, as this can cause issues with future reviews.
      
DO NOT drag logs directly into this text box, as we cannot read these!

3.  If you are fixing a performance issue, please include a link to a
    Timings and/or profiler report, both before and after your PR.

4.  If you are fixing a visual bug, such as in commands, please include
    screenshots so that we can more easily review the proposed fix.
    (You can drag screenshots into the bottom of the editor.)

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR fixes
multiple issues, you should repeat the phrase "fixes #nnnn" for each
issue.
-->

This PR fixes #6608. 

### Details

**Proposed fix:**    
<!-- Type a description of your proposed fix below this line. -->

`getAlternative` prunes collected `WeakReference`s through its own
iterator, which can remove
every entry in a bucket. The following `commands.get(0)` then throws
`IndexOutOfBoundsException`.

This stores the first live alias during the iteration and returns that
instead, or `null` when
nothing live remains, which is what the caller already tests for
(`getAlternative(label) == null` in
`EssentialsPlayerListener.CommandSendFilter`).

Confirmed with an instrumented build that logged the list sizes around
the prune loop:

```
[6608] label=w thread=Paper Async Command Builder Thread Pool - 0
       entry=2 removedByUs=2 after=0 expected=0
```

Both references in the `w` bucket had been collected and the loop
removed both.

Note: this does not address the possible concurrent modification also
raised in #6608
(`altCommands` is a plain `HashMap` of `ArrayList`, modified from the
async command-send path).
That needs a separate synchronisation decision.


**Environments tested:**    

<!-- Type the OS you have used below. -->
OS: Debian 12

<!-- Type the JDK version (from java -version) you have used below. -->
Java version:  25.0.4 2026-07-21 LTS

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
- [x] Most recent Paper version (26.2, git-Paper-112)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8


**Demonstration:**    
<!--
Below this block, include screenshots/log snippets from before and after
as
necessary. If you have created or used a test case plugin, please link
to a
download of the plugin, source code and exact version used where
possible.
-->

Before, on every boot with a player join (5 consecutive boots,
EssentialsX 2.22.0 and
2.22.1-dev+17-382de4f, same server and plugin set):

```
[ERROR]: Could not pass event AsyncPlayerSendCommandsEvent to Essentials
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
    at java.util.ArrayList.get(ArrayList.java:428)
    at com.earth2me.essentials.AlternativeCommandsHandler.getAlternative(AlternativeCommandsHandler.java:133)
    at com.earth2me.essentials.EssentialsPlayerListener$CommandSendFilter.isEssentialsCommand(EssentialsPlayerListener.java:1279)
    at net.ess3.provider.providers.PaperCommandSendListenerProvider.onAsyncCommandSend(PaperCommandSendListenerProvider.java:28)
...
```

Cause isolated with a temporary instrumented build logging list sizes
around the prune loop:

```
[6608] label=w thread=Paper Async Command Builder Thread Pool - 0
       entry=2 removedByUs=2 after=0 expected=0
```

Both `WeakReference` in the `w` bucket had been collected and the loop
removed both.

After, with this fix (2.22.1-dev+18), same server, player join:

```
IndexOutOfBoundsException:                0
AlternativeCommandsHandler references:    0
Could not pass event AsyncPlayerSendCommandsEvent: 0
```
2026-10-04 21:55:15 +00:00
Lewis GibsonandRaw2d 2dbd63adf8 Use PaperLib's asynchronous teleports and chunk loads on 26.x (#6634)
### Information

This PR fixes #6632.

### Details

**Proposed fix:**
PaperLib 1.0.6 reads the server version from `Bukkit#getVersion()` with
a regex that only accepts a single digit major version, so on 26.x it
reads the version as 0 and selects its synchronous fallbacks. Every
`PaperLib.teleportAsync`, `getChunkAtAsync` and
`getBedSpawnLocationAsync` call in EssentialsX then loads the chunk and
teleports on the calling thread instead of using Paper's asynchronous
API.

When PaperLib has not recognised a Paper server that is newer than 1.13,
EssentialsX now installs a PaperLib environment which selects the
handlers PaperLib would have chosen for 1.15.2 and later. Nothing
changes on servers where PaperLib reads the version correctly.

**Environments tested:**
OS: Ubuntu 24.04 (WSL2)
Java version: Temurin 25.0.4.1
- [x] Most recent Paper version (26.3, git-Paper-140)
- ~~CraftBukkit/Spigot/Paper 1.12.2~~
- ~~CraftBukkit 1.8.8~~

**Demonstration:**
`ModernPaperEnvironmentTest` checks that a 26.x version string leaves
PaperLib's own environment calling `Entity#teleport` and that the new
environment calls `Entity#teleportAsync` and `World#getChunkAtAsync`.
`./gradlew build` passes.

Handlers PaperLib selected on Paper 26.3 (build 140), read from the
bundled PaperLib by a test plugin:

Before (2.x, Paper 26.3 build 140):
```
PROBE paperlib env=PaperEnvironment version=0 asyncChunksHandler=AsyncChunksSync asyncTeleportHandler=AsyncTeleportSync isGeneratedHandler=ChunkIsGeneratedUnknown bedSpawnLocationHandler=BedSpawnLocationSync
```
After (this PR):
```
PROBE paperlib env=ModernPaperEnvironment version=0 asyncChunksHandler=AsyncChunksPaper_15 asyncTeleportHandler=AsyncTeleportPaper_13 isGeneratedHandler=ChunkIsGeneratedApiExists bedSpawnLocationHandler=BedSpawnLocationPaper
```
The same scripted run of EssentialsX commands (teleports across 12000
blocks, `/tpa`, `/home`, `/spawn`, `/back` after death, a passenger, 22
checks) passes before and after.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
2026-10-04 21:08:16 +00:00
Josh Roy cfb6f12da3 Fix economy lag message (#6629)
wasn't displaying the formatted time see #6583
2026-09-26 06:04:42 +00:00
Josh Roy 49a2f10071 Update to Minecraft 26.3 (#6624) 2026-09-15 15:43:50 +00:00
Keane Audric 43cb76a888 Fix MiniMessage and color formatting in disposal GUI title (#6616)
### Information

This PR fixes #6606.

### Details

**Proposed fix:**  
Passes the localized `disposal` string through
`AdventureFacet#miniToLegacy` before creating the chest inventory view
in both `Commanddisposal.java` and `SignDisposal.java`. This ensures
both legacy formatting codes (`&a&l`) and Adventure/MiniMessage tags
(`<green><bold>`) parse and render correctly in the inventory title bar.

**Environments tested:**  

OS: Windows 11  
Java version: Java 21  

- [x] Most recent Paper version (26.2)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8

**Demonstration:**  
<img width="554" height="76" alt="image"
src="https://github.com/user-attachments/assets/6bb5b6a2-2a7b-4cf8-8d54-15776e63e2dd"
/>

<img width="388" height="223" alt="image"
src="https://github.com/user-attachments/assets/bc8f5dee-9faa-4a03-937e-37d64a283e4c"
/>
2026-08-29 17:54:49 +00:00
Flask BotandMD 4600c72fd9 New Crowdin updates (#6554)
Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>
2026-08-26 19:22:03 +00:00
Josh Roy d5dc7e3a64 Fix trade sign validation mismatches on recharge (#6614) 2026-08-23 20:40:13 +00:00
Josh Roy 8013feb70c Fix newbie messages not firing for new players on modern Paper (#6571)
Player#hasPlayedBefore() is unreliable since the switch to
configuration-phase events, as player data is persisted during login, so
genuine first-joiners were treated as returning players. This broke
EssentialsSpawn newbie spawn/kit/announce-format (#6466).

Add a firstJoin flag to AsyncUserDataLoadEvent, derived from
EssentialsX's own user data, and use it in EssentialsSpawn and
EssentialsDiscord instead of hasPlayedBefore().

Fixes #6466
2026-08-22 20:50:36 +00:00
1wairesd 83aaf8cb37 Fix /exp command usages & tab complete (#6607) 2026-08-22 18:35:09 +00:00
Rasmus Kjær Damgaard 966bc999e3 Remove double spaces from alertFormat (#6612)
Fixes the last double-space string from #6611. This one is in the
English source, so Crowdin can't fix it: alertFormat has spaces on both
sides of <reset> and <primary>, so every alert line shows double spaces.
Italian and Korean already write it without them, the rest copied the
source. I'll fix those on Crowdin after.
2026-08-22 18:33:02 +00:00
Tyler ReschandJRoy 382de4ff3b Fix: /back onto nether roof exploit when destination is unsafe. (#6602)
<!--

EssentialsX bug fix submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a bug fix, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original bug
report. If there isn't an existing bug report, we recommend opening a
new
detailed bug report BEFORE opening your PR to fix it, else your PR may
be
      delayed or rejected without warning.
      
      You can open a new bug report by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose 

2.  When linking logs or config files, do not attach them to the post!
Copy and paste any logs into https://gist.github.com/, then paste a
link to them in the relevant parts of the template. Do not use Hastebin
      or Pastebin, as this can cause issues with future reviews.
      
DO NOT drag logs directly into this text box, as we cannot read these!

3.  If you are fixing a performance issue, please include a link to a
    Timings and/or profiler report, both before and after your PR.

4.  If you are fixing a visual bug, such as in commands, please include
    screenshots so that we can more easily review the proposed fix.
    (You can drag screenshots into the bottom of the editor.)

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR fixes
multiple issues, you should repeat the phrase "fixes #nnnn" for each
issue.
-->

This PR fixes #3318 and fixes #4483.

### Details

**Proposed fix:**    
<!-- Type a description of your proposed fix below this line. -->
- Created a crude `isAboveNetherRoof` safeguard for
`getSafeDestination`.

**Environments tested:**    

<!-- Type the OS you have used below. -->
OS: Linux (Server and Client)

<!-- Type the JDK version (from java -version) you have used below. -->
Java version: 25

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
- [ ] ~~Most recent Paper version (XX.YY.Z, git-Paper-BUILD)~~
- [ ] ~~CraftBukkit/Spigot/Paper 1.12.2~~
- [ ] ~~CraftBukkit 1.8.8~~
- [x] Paper 1.21.11


**Demonstration:**    
<!--
Below this block, include screenshots/log snippets from before and after
as
necessary. If you have created or used a test case plugin, please link
to a
download of the plugin, source code and exact version used where
possible.
-->
Before:


https://github.com/user-attachments/assets/250a27cf-1f11-421b-9d9f-36e0965249d3

After:


https://github.com/user-attachments/assets/38fc1db6-c364-4866-b08b-bfaa27fdaf5e

---------

Co-authored-by: JRoy <10731363+JRoy@users.noreply.github.com>
2026-08-08 20:47:14 +00:00
Josh Roy f6f13fdee8 Show muted players' private messages to social spies (#6569)
Muted players' /msg and /r attempts threw a "silenced" error before
reaching SimpleMessageRecipient, so the socialspy-listen-muted-players
option never applied to private messages.

Move mute handling into SimpleMessageRecipient#sendMessage: a muted
sender's message is dropped instead of delivered, the sender is notified
they're silenced, and social spies still see it with the muted prefix
when socialspy-listen-muted-players is enabled. PrivateMessageSentEvent
is not fired in this case so undelivered messages aren't relayed
elsewhere (e.g. Discord).

Also adds User#notifyMuted() to dedupe the silenced-message logic shared
with EssentialsPlayerListener.

Fixes #6341
2026-08-05 03:28:01 +00:00
Josh Roy d2a7e0f99d Update paperVersion to 26.2.build.92-stable (#6575) 2026-08-05 03:27:53 +00:00
NeglectDreamandJRoy 64cfb10954 Optimize cached online user lookups (#6595)
Co-authored-by: JRoy <10731363+JRoy@users.noreply.github.com>
2026-08-04 04:51:09 +00:00
Jason ee9f6f944a Fix Discord Config URLs (#6593)
Fixes #6592
2026-08-04 04:20:40 +00:00
Jason 776f7094a8 Update Minecraft version format in PR templates (#6581)
Embracing the future!
2026-06-24 22:29:19 +00:00
Josh Roy 28eb89fb02 Add toolchain auto download (#6574) 2026-06-16 15:34:32 +00:00
Josh Roy ff54e64909 Update to Minecraft 26.2 (#6561) 2026-06-16 15:18:18 +00:00
Josh Roy 15ec9a2589 Fix item components in /give on 26.1+ (#6570)
Fixes #6532
2026-06-14 20:44:32 +00:00
Josh Roy 38f1b4ddde Shutdown executors on disable (#6565)
Closes #6446
2026-06-14 19:20:28 +00:00
Josh Roy 37ce087a71 Fix 1.16 & 1.17 Compatability (#6562) 2026-06-14 19:19:37 +00:00
Josh Roy e604c9f2ae Fix teleport-cooldown not applying to /tp <x> <y> <z> (#6568)
Self-teleport to coordinates called now() with cooldown disabled, so it
ignored teleport-cooldown while every other self-teleport path enforces
it. Enable the cooldown flag for this case.

Fixes #6499
2026-06-14 19:00:02 +00:00
Josh Roy 49cc72ced2 Fix dump mirror comment posting (#6566) 2026-06-14 17:38:28 +00:00
Josh Roy 5e95c87fdf Fix nickname format codes bleeding into following text (#6564)
The single-arg legacyToMini() facet default flipped from false to true
during the Paper adventure migration (#6220), switching message
placeholder serialization from strict to non-strict. Non-strict output
leaves format tags open, so a formatted nickname/display name bleeds its
color and formatting into the rest of the message.

Restore the default to false and drop the now-redundant <reset>
workaround in SocialSpy local chat (#6507).

Fixes #6559
Fixes #6537
2026-06-14 07:59:54 +00:00
Josh Roy c28c654b1e Fix LuckPerms sync load for online players (#6563)
Fixes #6519
2026-06-14 07:58:53 +00:00
1wairesd 26a54475ad fix: replace PatternType.getByIdentifier() with PatternTypeProvider (#6553)
PatternType.getByIdentifier() causes IncompatibleClassChangeError on
1.19.4 due to a mismatch between Methodref and InterfaceMethodref
bytecode constants when compiled against a newer API.

Introduce PatternTypeProvider with two implementations:
- LegacyPatternTypeProvider: iterates PatternType.values() for 1.19.4
and older where PatternType is still an enum
- ModernPatternTypeProvider: uses Registry.BANNER_PATTERN for 1.20.5+

Update MetaItemStack.addBannerMeta to resolve PatternType through the
provider instead of calling the deprecated static method directly.

You need to select a pull request template!

If you're adding a new feature, copy and paste the following parameter
at the end of the URL:
    ?template=new-feature.md

If you're fixing a bug, copy and paste the following parameter at the
end of the URL:
    ?template=bug-fix.md

For more information about contributing to EssentialsX, see
CONTRIBUTING.md:
    https://github.com/EssentialsX/Essentials/blob/2.x/CONTRIBUTING.md
2026-06-07 20:02:55 +00:00
MD b303bf90d6 Bump version number to 2.22.1 for dev builds (#6551) 2026-05-31 14:58:05 +00:00
MD 100f575293 Release 2.22.0 (#6550) 2026-05-31 14:16:07 +00:00
c5a25bb79c New Crowdin updates (#6251)
Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>
2026-05-31 12:28:07 +00:00
Jason 5baf2391b3 Remove unused essentials.jail perm (#6539)
The `/jail` command is just an alias for `/togglejail`. As such, the
`essentials.jail` permission is never used.
2026-05-12 22:31:46 +00:00
Josh Roy c38c9256e9 Fix deposits rejected when account has negative balance (#6515)
Economy.add() delegated to setMoney() which checks minMoney and loan
permission against the resulting balance. Both checks reject negative
results even when the balance is improving from a deposit. Since
deposits can only move the balance upward, route them directly to
user.setMoney() bypassing the floor/loan checks. Withdrawals still
go through setMoney() with full validation.

Fixes #5606
2026-05-04 08:26:42 +00:00
Josh Roy 79d67e98c3 Fix essentials.home.bed permission being ignored (#6516)
Two paths bypassed the permission check:

1. Explicit /home bed: the permission check at line 44 was combined
   with the bed-spawn logic in a single condition. When denied, it
   fell through to goHome("bed") which found the stored bed home
   by name and teleported anyway. Now rejects immediately.

2. Fallback respawn: when a player has no homes and isSpawnIfNoHome
   is true, respawn() was called unconditionally which tries the bed.
   Now checks the permission and falls back to world spawn when denied.

Fixes #5963
2026-05-04 08:24:28 +00:00
Josh Roy 5db483b548 Fix Discord Link RoleSyncManager thread leak (#6512)
Two issues causing unbounded thread growth:

1. The repeating sync task was never cancelled on plugin disable,
   leaking the timer itself.

2. Each sync() call spawns an async task that calls
   acquireUninterruptibly() on a 5-permit semaphore. The timer fires
   up to 50 sync calls per cycle, so 45 threads block indefinitely
   waiting for permits. Before they drain, the next cycle spawns 50
   more. Threads accumulate until OOM.

Fix: cancel the task on disable, and replace acquireUninterruptibly()
with tryAcquire(5s timeout) in both sync() and unSync() so threads
don't block indefinitely — skipped syncs retry on the next cycle.

Fixes https://github.com/EssentialsX/Essentials/issues/6381
2026-05-04 08:23:08 +00:00
HotWaterFlaskandJosh Roy 39bc0fe224 Send geoip join messages to console (#6523)
Not only output to the players who have the "essentials.geoip.show"
permission, but also output to the console.

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-05-04 08:16:42 +00:00
Josh Roy 1a9cba913b Add lunge to /enchant (#6534)
Fixes #6521
2026-05-04 08:15:52 +00:00
Hudson AndrésandJosh Roy 24b7285e42 fix: reset nickname when player changes Minecraft account name (#6470)
Closes #6469

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-05-03 05:43:46 +00:00
Adrián Oliva Del RíoandJosh Roy 5c45ee2169 Add /jailedplayers command to list jailed players (#6517)
<!--

EssentialsX feature submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a new feature, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original feature
      request. If there isn't an existing feature request, we strongly
      recommend opening a new feature request BEFORE opening your PR to
implement it, as this allows us to review whether we're likely to accept
      your feature in advance, and also allows us to discuss possible
      implementations for the feature. If there is no associated feature
      request, your PR may be delayed or rejected without warning.
      
      You can open a new feature request by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose

2. If you are fixing a performance issue, please use the "Bug fix" PR
template
instead. The "bug fix" template is better suited to performance issues.

3.  Include a demonstration.
If you are adding commands, please provide screenshots and/or a video
demonstration of the feature. Similarly, if you are adding a new API,
      please include a link to example code that takes advantage of your
proposed API. This will aid us in reviewing PRs and speed up the process
      significantly.

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR
implements
features from multiple issues, you should repeat the phrase "closes
#nnnn"
    for each issue. 
-->

This PR closes #5838. 

### Details

**Proposed feature:**  
<!-- Type a description of your proposed feature below this line. -->
Create a command to query the list of imprisoned players

**Environments tested:**    

<!-- Type the OS you have used below. -->
OS:  Ubuntu 24.04.4 LTS x64 bits

<!-- Type the JDK version (from java -version) you have used below. -->
Java version:  JDK21

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
- [x] Most recent Paper version (1.XX.Y, git-Paper-BUILD)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8


**Demonstration:**  
<!--
Below this block, include screenshots/log snippets from before and after
as
necessary. If you have created or used a test case plugin, please link
to a
download of the plugin, source code and exact version used where
possible.
-->
**Jailed players execution with 2 players jailed who are offline:**
<img width="657" height="96" alt="image"
src="https://github.com/user-attachments/assets/45663c9f-5d87-43f7-94a2-cefca62d6c07"
/>

**Jailed players command execution with no players jailed:**
<img width="657" height="96" alt="image"
src="https://github.com/user-attachments/assets/10813887-b7ef-4291-ab12-8927d50b6a05"
/>

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-05-03 03:32:52 +00:00
Josh Roy 87bde72678 Fix consoleName translation key not working (#6506)
Fixes #6343
2026-04-12 21:15:37 +00:00
Josh Roy 97fd5609b4 Fix {USERNAME} underscore escaping in kit items (#6514)
The underscore-to-backslash escaping in KeywordReplacer is needed for
item names/lores where underscores represent spaces, but Minecraft
usernames never contain spaces and their underscores must stay literal.
Skip the escaping for USERNAME keywords so player:{USERNAME} resolves
correctly for names like Player_Name.

Fixes #6296
2026-04-12 21:10:46 +00:00
Josh Roy 936a2c0c62 Fix SocialSpy local chat color bleeding from player display name (#6507)
The display name converted via legacyToMini() produces open MiniMessage
formatting tags that bleed into the subsequent message text. Append
<reset> after the display name to close any inherited formatting.

Fixes #6504
2026-04-12 21:10:40 +00:00
Josh Roy 3d03cf35df Fix command descriptions not using player's locale (#6508)
Usage descriptions were resolved once at command construction time via
tlLiteral() (server default locale) and cached. Now store the
translation key instead and resolve per-player at display time using
playerTl()/tl().

Fixes #6353
2026-04-12 21:10:37 +00:00
Josh Roy 57a8c596f8 Fix /time overriding player's /ptime setting (#6509)
Fixes #6359
2026-04-12 21:10:34 +00:00
Josh Roy b594358545 Fix VanishStatusChangeEvent NPE and document parameter inversion (#6510)
The affected/controller parameters are swapped in Commandvanish (#2604)
but cannot be fixed without breaking the public API.

Fix BukkitListener to use getController() (the actual vanished player)
instead of getAffected() (the sender, null for console), and add
inversion warning javadoc matching the NickChangeEvent pattern.

Fixes #6388
2026-04-12 21:10:30 +00:00
Josh Roy e9f3677b95 Fix /eco ignoring locale-specific decimal separators (#6511)
Commandeco used raw BigDecimal constructor with hardcoded period as
decimal separator, breaking input like '1.000' for European locales. Use
NumberUtil.parseStringToBDecimal() like /pay already does.

Fixes #6475
2026-04-12 21:10:26 +00:00
Josh Roy 678bacfee5 Suppress advancement broadcast for vanished players on Paper (#6513)
Use Paper's event.message(null) API to hide the advancement chat message
when the player is vanished (checked via Bukkit metadata). The player
still receives the advancement, only the broadcast is suppressed.

Fixes #6350
2026-04-12 21:10:23 +00:00
Josh Roy 7f37b5e0f3 Fix minimessage parsing in playerlist keyword replacement (#6505)
Fixes #6502
2026-04-12 20:25:07 +00:00
Brokkonaut f39bc91ef5 Fix version parsing for local paper builds (#6501)
Fixes #6498
2026-04-12 17:42:15 +00:00
Jason 63e7c4d259 Bump supported versions to 26.1.2 (#6500) 2026-04-11 19:06:18 +00:00
Josh Roy 1038b3a597 Fix incorrect teleport safety message (#6486)
old message seems needlessly technical anyway 

Fixes #5088
2026-04-05 20:55:41 +00:00
Josh Roy fb669aa9fd Remove extractUrls from default legacy serializer (#6492)
extractUrls on the default LegacyComponentSerializer caused
legacyToMini to auto-wrap URLs in <click:open_url> tags. When
translation templates like discordCommandLink already wrapped {0}
in their own click tag, this produced nested click events that
Paper 1.21.11+ rejected.

The default serializer no longer extracts URLs. A new
legacyToMiniWithUrls method is available for cases that explicitly
need clickable URL extraction (chat, broadcast).

Fixes #6463
2026-04-05 20:55:38 +00:00
Josh Roy add5eb7cbf Add permission cache system + permission for vanish item pickup (#6488)
Fixes #6270
2026-04-05 20:55:36 +00:00
Josh Roy ddaadf2099 Add socialSpyCmdFormat message key for command social spy (#6489)
Fixes #6049
2026-04-05 17:41:54 +00:00
Josh Roy 4db09f2a2e Change default max-nick-length to 16 (#6484)
Fixes #6028
2026-04-05 17:41:51 +00:00
Josh Roy 00b4a5b3bd Make /flyspeed and /walkspeed force the correct speed type (#6485)
Fixes #4883
2026-04-05 17:41:47 +00:00
Josh Roy 73aa65733e Reject Windows reserved filenames in /setwarp (#6487)
Fixes #5430

stop gap for before we do #5267
2026-04-05 17:41:34 +00:00
Josh Roy 86621cd90e Don't enable fly mode on login when player is in water (#6490)
Fixes #6198
2026-04-05 17:41:33 +00:00
Josh Roy 236ce8837e Add /unmute command and broadcast on unmute (#6491)
Fixes #6419
2026-04-05 17:41:29 +00:00
TouchieandJRoy f5fc4305c9 fix(fly): persist fly mode and restore on join (#6403)
Persist /fly state in userdata and re-enable allowFlight on login so
flight isn’t lost after relogging on ground (upstream #6344).

<!--

EssentialsX bug fix submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a bug fix, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original bug
report. If there isn't an existing bug report, we recommend opening a
new
detailed bug report BEFORE opening your PR to fix it, else your PR may
be
      delayed or rejected without warning.
      
      You can open a new bug report by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose 

2.  When linking logs or config files, do not attach them to the post!
Copy and paste any logs into https://gist.github.com/, then paste a
link to them in the relevant parts of the template. Do not use Hastebin
      or Pastebin, as this can cause issues with future reviews.
      
DO NOT drag logs directly into this text box, as we cannot read these!

3.  If you are fixing a performance issue, please include a link to a
    Timings and/or profiler report, both before and after your PR.

4.  If you are fixing a visual bug, such as in commands, please include
    screenshots so that we can more easily review the proposed fix.
    (You can drag screenshots into the bottom of the editor.)

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR fixes
multiple issues, you should repeat the phrase "fixes #nnnn" for each
issue.
-->

This PR fixes #6344 

### Details

**Proposed fix:**    
<!-- Type a description of your proposed fix below this line. -->
Persist the /fly state to userdata and restore allowFlight on login so
flight isn’t lost after relogging while on the ground.

**Environments tested:**    

<!-- Type the OS you have used below. -->
OS: Ubuntu 24.04

<!-- Type the JDK version (from java -version) you have used below. -->
Java version: 21

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
- [x] Most recent Paper version (1.XX.Y, git-Paper-BUILD)
- [x] Paper 1.21.8
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8

---------

Co-authored-by: JRoy <10731363+JRoy@users.noreply.github.com>
2026-04-05 04:49:23 +00:00
Golfing7andJosh Roy 08b7fe3bb8 Add PreTransactionEvent (#6370)
<!--

EssentialsX feature submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a new feature, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original feature
      request. If there isn't an existing feature request, we strongly
      recommend opening a new feature request BEFORE opening your PR to
implement it, as this allows us to review whether we're likely to accept
      your feature in advance, and also allows us to discuss possible
      implementations for the feature. If there is no associated feature
      request, your PR may be delayed or rejected without warning.
      
      You can open a new feature request by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose

2. If you are fixing a performance issue, please use the "Bug fix" PR
template
instead. The "bug fix" template is better suited to performance issues.

3.  Include a demonstration.
If you are adding commands, please provide screenshots and/or a video
demonstration of the feature. Similarly, if you are adding a new API,
      please include a link to example code that takes advantage of your
proposed API. This will aid us in reviewing PRs and speed up the process
      significantly.

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR
implements
features from multiple issues, you should repeat the phrase "closes
#nnnn"
    for each issue. 
-->

I added a PreTransactionEvent for cancelling transactions between users.

### Details

**Proposed feature:**  
Adding a PreTransactionEvent.


**Environments tested:**    

<!-- Type the OS you have used below. -->
OS: Linux Mint

<!-- Type the JDK version (from java -version) you have used below. -->
Java version:  23

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
- [x] Most recent Paper version (1.XX.Y, git-Paper-BUILD)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8


**Demonstration:**  
<!--
Below this block, include screenshots/log snippets from before and after
as
necessary. If you have created or used a test case plugin, please link
to a
download of the plugin, source code and exact version used where
possible.
-->
```java
    @EventHandler
    public void onPay(PreTransactionEvent event) {
        // Prevent money from being sent outside their own faction.
        IUser userSource = event.getRequester().getUser();
        if (userSource == null)
            return;

        FPlayer fplayerSource = FPlayers.getInstance().getByPlayer(userSource.getBase());
        if (fplayerSource == null || fplayerSource.isAdminBypassing())
            return;

        Faction faction = fplayerSource.getFaction();
        if (faction == null || !isFactionVoluntaryExcluded(faction.getId()))
            return;

        // Who are they sending it to?
        FPlayer target = FPlayers.getInstance().getById(event.getTarget().getUUID().toString());
        Faction factionTarget = target.getFaction();
        if (faction != factionTarget) {
            event.setCancelled(true);
            msg(userSource.getBase(), cantPayOutsideExcludedFactionMessage);
        }
    }
```
This code prevents transactions between two users when two factions
can't interact.

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-04-05 03:46:06 +00:00
CrystalD5andJosh Roy 0845ea6f08 Fixed NoSuchMethodException Player.getRespawnLocation() (#6460)
Fixes #6459.

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-04-05 02:57:29 +00:00
vbojovicandJosh Roy 5a78df5494 Add option to allow fishing when afk (#6465)
Fixes #6443
Fixes #6323

- Prevent AFK timer reset when fishing rod bobber is idle
- Fix incorrect teleportOfflineUnknown message showing sender instead of
target

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-04-05 02:36:13 +00:00
Josh Roy 716a61eb16 Mitigate Discord 429s (#6461) 2026-04-04 19:17:36 +00:00
Josh Roy 637ac8f4ee Fix version comparison logic for pre-release/snapshot/rc variants (#6483)
Pre-release, snapshot, and RC variants should be considered lower than
the base release version. Updated test assertions and comparison logic
to reflect that release versions are higher than their dev variants
(snapshot < pre < rc < release).
2026-04-04 18:58:55 +00:00
Jason 218e004108 Bump supported versions to 26.1.1 (#6482)
Yay!
2026-04-04 05:28:07 +00:00
Jason c26909e2b8 Add alias "ver" for the "version" subcommand (#6481)
Fixin what u broke :)
2026-04-04 04:48:36 +00:00
5d535dcbe4 Support new PaperMC versioning scheme in VersionUtil (#6480)
ThioJoe is waiting!
Re #6479

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JRoy <10731363+JRoy@users.noreply.github.com>
2026-04-04 04:11:15 +00:00
Josh Roy 8e3b24fdba Minecraft 26.1 (#6473) 2026-03-23 18:07:48 +00:00
CopilotandJRoy d7452bfcc7 Fix stale shadow include group ID for emoji-java in EssentialsDiscord (#6467)
Commit 297d0e4 migrated `emoji-java` from Jitpack
(`com.github.MinnDevelopment`) to EssentialsX's own Maven repo
(`net.essentialsx.deps`), but the `include(dependency(...))` in the
`shadowJar` block was not updated to match, leaving the shadow JAR
unable to locate and bundle the dependency.

### Details

**Proposed fix:**
- Updated `EssentialsDiscord/build.gradle` shadowJar include to use the
correct group ID:
  ```groovy
  // Before
  include(dependency('com.github.MinnDevelopment:emoji-java'))

  // After
  include(dependency('net.essentialsx.deps:emoji-java'))
  ```

**Environments tested:**    

OS: 

Java version:  

- [ ] Most recent Paper version (1.XX.Y, git-Paper-BUILD)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8


**Demonstration:**

<!-- START COPILOT CODING AGENT SUFFIX -->



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> never updated the shadow include group id here pls fix
> 
> Reference:
https://github.com/EssentialsX/Essentials/commit/297d0e460605efec9db2ca824ad20945838a1ff3


</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

📱 Kick off Copilot coding agent tasks wherever you are with [GitHub
Mobile](https://gh.io/cca-mobile-docs), available on iOS and Android.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JRoy <10731363+JRoy@users.noreply.github.com>
2026-03-15 15:49:56 +00:00
482175e6c2 Prevent running recipe command while sleeping (#6455)
<!--

EssentialsX bug fix submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a bug fix, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original bug
report. If there isn't an existing bug report, we recommend opening a
new
detailed bug report BEFORE opening your PR to fix it, else your PR may
be
      delayed or rejected without warning.
      
      You can open a new bug report by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose 

2.  When linking logs or config files, do not attach them to the post!
Copy and paste any logs into https://gist.github.com/, then paste a
link to them in the relevant parts of the template. Do not use Hastebin
      or Pastebin, as this can cause issues with future reviews.
      
DO NOT drag logs directly into this text box, as we cannot read these!

3.  If you are fixing a performance issue, please include a link to a
    Timings and/or profiler report, both before and after your PR.

4.  If you are fixing a visual bug, such as in commands, please include
    screenshots so that we can more easily review the proposed fix.
    (You can drag screenshots into the bottom of the editor.)

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR fixes
multiple issues, you should repeat the phrase "fixes #nnnn" for each
issue.
-->

This PR fixes #6450 . 

### Details

**Proposed fix:**    
I added a check to make sure that items are only placed into the
inventory when it successfully opens.


**Environments tested:**    

<!-- Type the OS you have used below. -->
OS: Linux Mint

<!-- Type the JDK version (from java -version) you have used below. -->
Java version:  21

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
- [x] Most recent Paper version (1.XX.Y, git-Paper-BUILD)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8


**Demonstration:**    
<!--
Below this block, include screenshots/log snippets from before and after
as
necessary. If you have created or used a test case plugin, please link
to a
download of the plugin, source code and exact version used where
possible.
-->
In order to replicate the issue, I used the following code to test:
```java
    @EventHandler
    public void onRecipeStop(InventoryOpenEvent event) {
        if (event.getPlayer().isSleeping()) {
            event.setCancelled(true);
        }
    }
```

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
Co-authored-by: Benedikt Johannes <benedikt.johannes.hofer@gmail.com>
2026-03-14 15:23:54 +00:00
CopilotandJRoy 302653a77c Fix workflow artifact upload paths missing "X" in EssentialsX addon jar names (#6462)
### Information

Workflow artifact upload steps referenced incorrect jar filenames — all
addon jars are built as `EssentialsX{Addon}-*.jar` (e.g.
`EssentialsXChat`, `EssentialsXProtect`) because project names in
`settings.gradle.kts` are all prefixed with `EssentialsX`, and
`archivesName` is set to `project.name`. The upload globs were missing
the `X`, causing every per-module direct upload to fail to find its
target file.

### Details

**Proposed fix:**  
Update all per-module `archive: false` upload step paths in both
`build-pr.yml` and `build-master.yml` to use the correct
`EssentialsX`-prefixed names:

| Before | After |
|---|---|
| `jars/EssentialsAntiBuild-*.jar` | `jars/EssentialsXAntiBuild-*.jar` |
| `jars/EssentialsChat-*.jar` | `jars/EssentialsXChat-*.jar` |
| `jars/EssentialsDiscord-*.jar` | `jars/EssentialsXDiscord-*.jar` |
| `jars/EssentialsDiscordLink-*.jar` |
`jars/EssentialsXDiscordLink-*.jar` |
| `jars/EssentialsGeoIP-*.jar` | `jars/EssentialsXGeoIP-*.jar` |
| `jars/EssentialsProtect-*.jar` | `jars/EssentialsXProtect-*.jar` |
| `jars/EssentialsSpawn-*.jar` | `jars/EssentialsXSpawn-*.jar` |
| `jars/EssentialsXMPP-*.jar` | `jars/EssentialsXXMPP-*.jar` |

The Javadoc `cp` paths in `build-master.yml` reference filesystem
directory names (which don't include `X`) and remain correct as-is.

**Environments tested:**

OS: N/A (CI workflow change only)

Java version: N/A

- ~~Most recent Paper version (1.XX.Y, git-Paper-BUILD)~~
- ~~CraftBukkit/Spigot/Paper 1.12.2~~
- ~~CraftBukkit 1.8.8~~

**Demonstration:**

N/A — the incorrect globs would silently produce no artifact (or an
error) for each addon module on every workflow run using `archive:
false`.

<!-- START COPILOT CODING AGENT SUFFIX -->



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

>
https://github.blog/changelog/2026-02-26-github-actions-now-supports-uploading-and-downloading-non-zipped-artifacts/
> 
> update our workflows to use this rather than zipping (still provide
the zip but also individually upload


</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for
you](https://github.com/EssentialsX/Essentials/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JRoy <10731363+JRoy@users.noreply.github.com>
2026-03-10 00:33:35 +00:00
Théo Dufrene 9d70fdb263 Add MiniMessage support for thunder enabled / disabled (#6454)
<!--

EssentialsX bug fix submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a bug fix, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original bug
report. If there isn't an existing bug report, we recommend opening a
new
detailed bug report BEFORE opening your PR to fix it, else your PR may
be
      delayed or rejected without warning.
      
      You can open a new bug report by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose 

2.  When linking logs or config files, do not attach them to the post!
Copy and paste any logs into https://gist.github.com/, then paste a
link to them in the relevant parts of the template. Do not use Hastebin
      or Pastebin, as this can cause issues with future reviews.
      
DO NOT drag logs directly into this text box, as we cannot read these!

3.  If you are fixing a performance issue, please include a link to a
    Timings and/or profiler report, both before and after your PR.

4.  If you are fixing a visual bug, such as in commands, please include
    screenshots so that we can more easily review the proposed fix.
    (You can drag screenshots into the bottom of the editor.)

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR fixes
multiple issues, you should repeat the phrase "fixes #nnnn" for each
issue.
-->

This PR fixes missing MiniMessage support for the enabled / disabled
thunder message

### Details

**Proposed fix:**    
<!-- Type a description of your proposed fix below this line. -->
Added parsing

**Environments tested:**    

<!-- Type the OS you have used below. -->
OS: Linux
<!-- Type the JDK version (from java -version) you have used below. -->
Java version:  21

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
- [x] 1.21.8 Paper version 
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8


**Demonstration:**    
<!--
Below this block, include screenshots/log snippets from before and after
as
necessary. If you have created or used a test case plugin, please link
to a
download of the plugin, source code and exact version used where
possible.
-->
2026-03-09 15:03:43 +00:00
MD 297d0e4606 Migrate Vault and emoji-java dependencies away from Jitpack (#6458)
Jitpack is down. Move dependencies to our own repo to remove the
build-time dependency on Jitpack.
2026-03-08 21:44:39 +00:00
UDPSendToFailed 9c833336fb Fix integer overflow with /burn + /more & fix % in chat formats (#6452)
Before:

- Using `/burn <player> 107374183` extinguished the player.
- `/burn` accepted negative values.
- Using `/more 2147483647` deleted the held item(s) entirely.

After:

- Using `/burn <player> 107374183` sets the player on fire for 107374183
seconds correctly.
- `/burn` doesn't accept negative values.
- Using `/more 2147483647` correctly increases the held item stack to
64.
2026-03-07 20:09:13 +00:00
UDPSendToFailedandMD 5abe8b61ef Fix server freezing with /give (#6449)
<!--

EssentialsX bug fix submission guide
====================================

NOTE: Failure to fill out this template properly may result in your PR
being
      delayed or ignored without warning.

NOTE: Don't type between any arrows in the template, as this text will
be
hidden. This includes this header block and any other explanation text
      blocks.

Want to discuss your PR before submitting it? Join the EssentialsX
Development
server: https://discord.gg/CUN7qVb


EssentialsX is GPL
------------------

By contributing to EssentialsX, you agree to license your code under the
GNU General Public License version 3, which can be found at the link
below:
https://github.com/EssentialsX/Essentials/blob/2.x/LICENSE


Instructions
------------

If you are submitting a bug fix, please follow the following steps:

1.  Fill out the template in full.
This includes providing screenshots and a link to the original bug
report. If there isn't an existing bug report, we recommend opening a
new
detailed bug report BEFORE opening your PR to fix it, else your PR may
be
      delayed or rejected without warning.
      
      You can open a new bug report by following this link:
      https://github.com/EssentialsX/Essentials/issues/new/choose 

2.  When linking logs or config files, do not attach them to the post!
Copy and paste any logs into https://gist.github.com/, then paste a
link to them in the relevant parts of the template. Do not use Hastebin
      or Pastebin, as this can cause issues with future reviews.
      
DO NOT drag logs directly into this text box, as we cannot read these!

3.  If you are fixing a performance issue, please include a link to a
    Timings and/or profiler report, both before and after your PR.

4.  If you are fixing a visual bug, such as in commands, please include
    screenshots so that we can more easily review the proposed fix.
    (You can drag screenshots into the bottom of the editor.)

-->

### Information

<!--
Replace #nnnn with the number of the original issue. If this PR fixes
multiple issues, you should repeat the phrase "fixes #nnnn" for each
issue.
-->

This PR fixes a bug where anyone with access to the /give command could
freeze the server.

### Details

Currently, if a user has permission to spawn items with metadata, they
can freeze or completely crash the server by supplying a massive integer
for a book's page number (e.g., `/give <player> written_book 1
page2147483647:payload`).

Because there is no upper bound check in `MetaItemStack.java`, the
internal `for` loop tries to allocate an `ArrayList` with billions of
empty strings to reach that requested page index. This instantly
exhausts the JVM heap and triggers an OutOfMemoryError, hard-crashing
the server thread, or at least freezing it entirely for seconds.

This PR fixes the issue by introducing a hard limit of 100 pages for
book metadata parsing. If the requested page exceeds this limit, the
loop doesn't execute and it instead throws a newly added
`pageLimitExceeded` translatable exception.


**Environments tested:**    

<!-- Type the OS you have used below. -->
OS:  Windows 11 25H2 26200.7922

<!-- Type the JDK version (from java -version) you have used below. -->
Java version: Java 21 (OpenJDK 64-Bit Server VM 21.0.10+7-LTS; Eclipse
Adoptium Temurin-21.0.10+7)

<!--
Put an "x" inside the boxes for the server software you have tested this
bug fix on. If this feature does not apply to a server, strike through
the server software using ~~strikethrough~~. If you have tested on other
    environments, add a new line with relevant details.
-->
Purpur version 1.21.11-2545


**Demonstration:**    
<!--
Below this block, include screenshots/log snippets from before and after
as
necessary. If you have created or used a test case plugin, please link
to a
download of the plugin, source code and exact version used where
possible.
-->

Before:

<img width="1401" height="269" alt="image"
src="https://github.com/user-attachments/assets/9453a449-af43-4e77-bdfe-26e63e47b634"
/>

After:

<img width="1104" height="189" alt="image"
src="https://github.com/user-attachments/assets/f1783011-92db-4518-9a4d-a831bb847261"
/>

Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>
2026-02-28 23:03:48 +00:00
Gavin HeatheringtonandJosh Roy b8acd9c3ab Allow discord console relay to be filtered by logger name (#6448)
Rearranged logger addition to message entry for filtering by logger
name.

---------

Co-authored-by: Josh Roy <10731363+JRoy@users.noreply.github.com>
2026-02-28 21:22:50 +00:00
BestZige f080f815fa fix(commands): propagate Essentials and module to tree nodes (#6445)
### What does this PR do?

Fixes an issue where `EssentialsTreeNode` instances did not receive
updated
`Essentials` and `IEssentialsModule` references after registration.

This could lead to null references during command execution,
especially in test environments.

### Changes made

- Override `setEssentials` in `EssentialsTreeCommand`
- Override `setEssentialsModule` in `EssentialsTreeCommand`
- Propagate updated references to all registered nodes

### Why is this needed?

Nodes only received references during registration.
If the parent command updated its references later,
the nodes would retain outdated (null) values.

### Does this PR break compatibility?

No.
2026-02-23 17:14:49 +00:00
Josh Roy 18549a718a Fix /ess reload (#6444) 2026-02-22 21:36:39 +00:00
202 changed files with 7166 additions and 2506 deletions

No files matched your search

+270
View File
@@ -0,0 +1,270 @@
---
name: mc-update
description: Update EssentialsX for a new Minecraft version. Use when the user asks to update to, bump, or support a new Minecraft or Paper version (for example "update to 26.4", "add 26.3 mobs", "bump supported version"), or asks what must be checked for new mobs, items, wood types, armor, enchantments, potions, or Bukkit API changes.
---
# Minecraft update
This checklist comes from every Minecraft update PR from 1.14 to 26.3, and from the fix PRs that followed them.
Paths are relative to the repository root. `ESS` means `Essentials/src/main/java/com/earth2me/essentials`.
## Procedure
1. Find the old and new `paper-api` jars in `~/.gradle/caches/modules-2/files-2.1/io.papermc.paper/paper-api/`. If the new jar is not there, set `paperVersion` in `build-logic/src/main/kotlin/essentials.base-conventions.gradle.kts` and run `./gradlew :EssentialsX:compileJava` to download it.
2. Diff the constants of these classes between the two jars: `org.bukkit.entity.EntityType`, `org.bukkit.Material`, `org.bukkit.enchantments.Enchantment`, `org.bukkit.potion.PotionEffectType`, `org.bukkit.potion.PotionType`, `org.bukkit.TreeType`, and each `*$Variant` class under `org.bukkit.entity`. Use this command for each class:
```sh
unzip -p <jar> org/bukkit/entity/EntityType.class > /tmp/x.class && javap -p /tmp/x.class | grep 'public static final'
```
3. Sort each added, removed, or renamed constant into a section of part 2 or part 3 below.
4. Do part 1. Then do each section that applies.
5. Apply part 4 to every line you write.
6. Run `./gradlew :EssentialsX:compileJava`. Fix errors.
7. Do not commit unless the user asks.
8. Report per section: what changed, and what you skipped and why.
---
## 1. Every update
### 1.1 Version constants
1. Open `ESS/utils/VersionUtil.java`.
2. Add a constant `vXX_Y_R01 = BukkitVersion.fromString("XX.Y-R0.1-SNAPSHOT")`.
- For a patch release, change the string of the existing minor constant. Example: `v26_1_R01` holds `26.1.2`.
- For a minor release, add a new constant. Do not rename or delete an old constant. Old constants gate features. Deleting `v1_20_4_R01` caused #6460.
3. Add the constant to `supportedVersions`. Keep the `ImmutableSet.of(...)` syntax. `build-logic/src/main/kotlin/VersionDataTask.kt` parses it with a regex.
4. Start a server. Confirm `BukkitVersion.fromString(Bukkit.getBukkitVersion())` parses. Spigot 1.14.3 and Paper 26.1 both changed the string format.
### 1.2 Documentation
1. Update the supported versions sentence in `README.md`.
2. Regenerate `items.json` with ItemDbGenerator.
---
## 2. New content
### 2.1 New mob
1. Add an entry to `ESS/Mob.java`: `NAME("CamelName", Enemies.X, "ENTITY_TYPE")`.
- Use the string constructor. Do not use `EntityType.X`. The constant does not exist on old servers.
- Set the plural suffix if it is not `s`. Fish use `""`. Fox uses `es`.
- The display name must not contain a space.
- Boats and rafts are also `Mob` entries.
2. Add `EntityType` constant to `ESS/MobCompat.java` only if `MobData` needs it.
3. Add the mob to `protect.prevent.spawn` in `Essentials/src/main/resources/config.yml`.
4. Check `entityToDisplayName` in `providers/BaseProviders/.../SpawnerItemProvider.java` if the enum name does not title-case well.
### 2.2 New mob variant or data
1. Add entries to `ESS/MobData.java`. Use the string form `"kind:VARIANT"`. Example: `"wolf:PALE"`.
2. Add a `case "kind":` to the `setData` switch.
3. Add `setKindVariant(Entity, String)` to `MobCompat.java`. Gate it with `VersionUtil.getServerBukkitVersion().isLowerThan(vXX_Y_R01)`.
4. Use `RegistryUtil.valueOf(Variant.class, name)` for registry types. Do not use `Enum.valueOf`.
5. Do not call `NewClass.Variant.values()` in an enum initializer. Axolotl did this. It threw `NoClassDefFoundError` on 1.16 (#4376).
6. Catch `Throwable` around reflective lookups. Cow variant threw NPE on old servers (#6121).
7. For a boolean flag, add a `Data` constant and a branch in `setData`. Example: `GOAT_SCREAMING`.
### 2.3 New rideable or saddle mob
1. Add `SADDLE_<MOB>` to `MobData.java` with a `Data.<MOB>SADDLE` constant.
2. Add `set<Mob>Saddle(Entity, Player)` to `MobCompat.java`. Tame the mob. Set the owner. Set the saddle.
3. Check `ESS/craftbukkit/Inventories.java` if the mob adds a player inventory slot. Body and saddle slots (41, 42) broke `/clearinventory` in 1.21.6 (#6218).
### 2.4 New item or block
1. Check each `MaterialUtil` set that lists names by hand. See table.
2. Check `ESS/utils/LocationUtil.java`:
- Add a block that hurts a standing player to `DAMAGING_TYPES`. Magma, campfire, berry bush and wither rose were missed for 2 years (#3537).
- `HOLLOW_MATERIALS` comes from `Material.isTransparent()`. Add a passable block that is not transparent. Example: `LIGHT` (#4601). Remove a transparent block that is solid. Examples: `BARRIER`, `DIRT_PATH`, `FARMLAND`.
3. Check for a new event that reveals player position. Sculk sensor needed vanish handling in `EssentialsPlayerListener` (#5262).
| `ESS/utils/MaterialUtil.java` set | Add when |
|---|---|
| `HELMETS`, `CHESTPLATES`, `LEGGINGS`, `BOOTS` | New armor tier |
| `LEATHER_ARMOR` | New dyeable armor |
| `BEDS` | New bed. `STRAW_BED` in 26.3. |
| `BANNERS` | New banner colour |
| `SIGN_POSTS`, `WALL_SIGNS`, `HANGING_SIGNS`, `HANGING_WALL_SIGNS` | New wood type |
| `MOB_HEADS` | New head. `PIGLIN_HEAD` in 1.20. |
| `POTIONS` | New potion container |
| `EDITABLE_BOOKS` | New book type |
| `FIREWORKS`, `FIREWORK_CHARGE` | New firework item |
`EnumUtil.getAllMatching` skips a name that does not exist. Adding a name is safe on old servers.
### 2.5 Renamed item, block or entity
1. Grep `ESS` for `Material.OLD_NAME` and `EntityType.OLD_NAME`. Replace with `EnumUtil.getMaterial("NEW", "OLD")` or a `MobCompat` constant. `EntityType.PIG_ZOMBIE` in a static map broke 1.16 (#3412). `EntityType.FIREWORK` broke `/firework` in 1.20.6.
2. For a renamed `EntityType`, add `getEntityType("NEW", "OLD")` to `MobCompat.java`. Update `Mob.java`, `MobData.java` and `SpawnMob.java`.
### 2.6 New wood type
1. Add `<WOOD>_SIGN`, `<WOOD>_WALL_SIGN`, `<WOOD>_HANGING_SIGN`, `<WOOD>_WALL_HANGING_SIGN` to the four sign sets in `MaterialUtil.java`. Crimson and warped were missed in 1.16 (#3487).
2. Add `<WOOD>_BOAT` to `Mob.java`. Boats are separate entity types since 1.21.3.
3. Add `<WOOD>` to `MobCompat.BoatVariant` for servers before 1.21.3.
4. Add `<WOOD>_BOAT` to `MobData.java`.
5. Add `<WOOD>_CHEST_BOAT` to `Mob.java`.
### 2.7 New tree type
1. `Commandtree` reads `TreeType.values()`. It needs no code change.
2. Add the name to `treeCommandUsage` and `treeCommandUsage1` in `messages.properties`.
3. `Commandbigtree` has four hard-coded types. Add a case if the tree has a big variant.
4. Bukkit adds `TreeType` late. 26.3-pre-2 has no `POPLAR`.
### 2.8 New armor tier
1. Add the four pieces to `HELMETS`, `CHESTPLATES`, `LEGGINGS`, `BOOTS` in `MaterialUtil.java`.
2. Add a branch to the armor keyword chain in `ESS/SpawnMob.java`. The chain has `netherite`, `copper`, `diamond`, `gold`, `leather`, `iron`.
3. For horse armor, add `<TIER>_HORSE_ARMOR` to `MobData.java`. Example: `NETHERITE_HORSE_ARMOR`.
### 2.9 New enchantment
1. Add a `try { // XX.Y }` block to `ESS/Enchantments.java`.
2. Look up by name: `Enchantment.getByName("NAME")`. Check for null. Do not use `Enchantment.NAME`.
3. Add `ENCHANTMENTS.put("name", e)` and `ALIASENCHANTMENTS.put("alias", e)`.
4. Swift sneak (1.19) and lunge (1.21.11) were missed. Contributors added them months later.
5. Display and permission names come from `Enchantments.getRealName()`. It uses the key on 1.13+.
### 2.10 New potion effect or potion type
1. Add a `try { // XX.Y }` block to `ESS/Potions.java`. Catch `Throwable`.
2. Add `POTIONS.put("name", PotionEffectType.NAME)` and `ALIASPOTIONS.put(...)`.
3. `ModernPotionMetaProvider` maps `LONG_` and `STRONG_` prefixes. Check a new potion follows that pattern.
### 2.11 New banner pattern
No code change. `PatternTypeProvider` reads the registry.
### 2.12 New colour family
Check `MaterialUtil.getColorOf` for a colour name that is not a `DyeColor`.
### 2.13 New spawn egg
No code change. `FlatSpawnEggProvider` derives the egg from `EntityType.name() + "_SPAWN_EGG"`.
### 2.14 New command for new content
1. Gate the command with `VersionUtil`. Return `tl("unsupportedFeature")` on old servers. Examples: `/beezooka`, `/ice`.
2. Add the command to `plugin.yml` and `messages.properties`. Check message parameters. `iceOther` shipped without `{0}`.
---
## 3. API changes
### 3.1 Enum becomes a registry
Enchantment, PotionEffectType, Sound, PatternType, Villager.Profession, Cat.Type, Biome and Boat.Type all changed.
1. Replace `EnumUtil.valueOf` with `RegistryUtil.valueOf(Class, names...)`. Replace `.values()` with `RegistryUtil.values(Class)`.
2. If a static method moved from an enum to an interface, code compiled against the new API throws `IncompatibleClassChangeError` on old servers. `catch (Exception)` does not catch it. `PatternType.getByIdentifier` did this (#6553).
3. For such a case, add a provider pair:
- Interface in `providers/BaseProviders/src/main/java/net/ess3/provider/`.
- `Modern*Provider` in BaseProviders with `@ProviderData(weight = 1)` and a `@ProviderTest`.
- `Legacy*Provider` in `providers/1_12Provider` or `providers/1_8Provider`. These compile against the old API.
- Register with `providerFactory.registerProvider(Legacy.class, Modern.class)` in `Essentials.java`.
4. In a `@ProviderTest`, use `getMethod`, not `getDeclaredMethod`. 1.21 moved spawner methods to a super-interface (80bdc5269).
### 3.2 Removed API class
`PotionData`, `Potion`, `BannerMeta.setBaseColor` and `Achievement` were removed.
1. Move code that uses the removed class to a provider in `1_12Provider` or `1_8Provider`.
2. Add a modern provider in BaseProviders.
3. Remove the method from `OfflinePlayer`, `FakeWorld` and `FakeServer` if it was an override.
### 3.3 Changed method signature
1. Read the Bukkit diff for changed return types on methods EssentialsX calls.
2. A changed return type changes the call descriptor. The call throws `NoSuchMethodError` on old servers. `PlayerPickupArrowEvent#getArrow` did this (#3175). `InventoryView` becoming an interface did this (#5851).
3. Use a provider or reflection for the changed call.
### 3.4 New API methods on Player, World, Server
1. Add stubs to `providers/1_8Provider/.../OfflinePlayer.java` and `providers/1_12Provider/.../FakeWorld.java` if they fail to compile.
2. Tests use MockBukkit since 1.21.6. `FakeServer` no longer needs updates.
### 3.5 New event
1. Put the handler in a separate `*Listener_XX_Y` class.
2. Register it behind a `VersionUtil` check or `Class.forName`.
3. Do not put a new event in a listener that loads on old servers. `EntityTransformEvent` failed on 1.13.1 (#2942).
### 3.6 Spigot NMS obfuscation
1. Add `VXX_Y_RN` to `providers/NMSReflectionProvider/.../ReflUtil.java` if Spigot bumps the NMS revision.
2. Update the obfuscated `MinecraftServer#isRunning` name table in `ReflServerStateProvider.java`. This broke on 1.18, 1.19, 1.19.3, 1.20.6 and 1.21.10.
3. Paper uses `PaperServerStateProvider`. Test on Spigot to see this fail.
### 3.7 Paper internal classes
Paper config classes move. `ReflOnlineModeProvider` broke on 1.19 when config moved to `io.papermc.paper.configuration.GlobalConfiguration`. Check every reflective Paper access.
### 3.8 World height
1. Do not hard-code `0`, `-1`, `128` or `256` as a Y bound.
2. Use `WorldInfoProvider.getMinHeight`, `getMaxHeight` and `getLogicalHeight`.
3. Grep `LocationUtil`, `RandomTeleport`, `Commandtop`, `Commandsethome`, `Teleport`, `AsyncTeleport` and `SpawnMob`. 1.18 needed three fix PRs (#4657, #4667, #4715).
### 3.9 Item data components
1. Check `MetaItemStack.parseStringMeta`. The `[...]` component branch calls `UnsafeValues#modifyItemStack`. Its argument shape changed in 1.20.5 and again in 26.1 (#6570).
2. Check `Commandbook`. Writable and written books have separate meta since 1.20.6 (#6064).
3. Check `Commandskull`. Skull owner needs a resolved profile since 1.20.4 (#6188).
4. Check `MetaItemStack.canSpawn()`.
### 3.10 Vanilla messages
Check the vanilla changelog for a new client message that duplicates an EssentialsX message. 1.15 added "Respawn point set". `EssentialsPlayerListener` now gates `bedSet` (#2943).
### 3.11 Version string format
1. `BukkitVersion.VERSION_PATTERN` in `VersionUtil.java` must parse the new string.
2. Supported suffixes: `-snapshot-N`, `-pre-N`, `-rc-N`, `.build.N-channel`, `.local`, `-R0.1-SNAPSHOT`.
3. `compareDevSpecifier` orders `snapshot < pre < rc < release`.
4. `isSupportedVersion` compares major, minor and patch. A patch release needs a constant change.
5. 26.1 needed four fix PRs for this (#6480, #6483, #6500, #6501).
### 3.12 Adventure and Paper native paths
Each Paper API shift produced a gate:
- `Essentials.initAdventureFacet` gates the Paper facet at `v1_18_2_R01` (#6562).
- `EssentialsSpawnPlayerListener` gates `getRespawnLocation` at `v1_20_4_R01` (#6460).
- `EssentialsPlayerListener` gates configuration events at `v1_21_8_R01`.
Check each gate against the new version.
---
## 4. Compatibility rules
EssentialsX runs on 1.8.8 through the current version. Every update PR must keep that.
1. Do not reference a new `Material`, `EntityType`, `Enchantment` or `PotionEffectType` constant in a static field or enum initializer. Use `EnumUtil.getMaterial`, `EnumUtil.getEntityType` or a name lookup.
2. Catch `Throwable`, not `Exception`, around reflective lookups and provider selection. A swallowed `NoSuchFieldError` selected the wrong provider (7d6195375).
3. Use only Guava and Gson methods that exist in 1.8.8. `Ints.constrainToRange` and `JsonArray#add(String)` do not (#4708, 576866540).
4. Compile legacy providers in `1_8Provider` or `1_12Provider`. Code compiled against the new API can carry the wrong bytecode.
5. Gate with `isLowerThan(<next major>)`, not `isLowerThanOrEqualTo(<patch>)`. Forks use nonstandard revision numbers (43eff69a2).
6. Do not delete a `VersionUtil` constant.
7. Test on Paper latest, Spigot latest, 1.12.2 and 1.8.8. Most follow-up fixes in this history came from old servers.
---
## 5. Test
1. Run `./gradlew test`.
2. Run `./gradlew runServer`.
3. Run `/ess version`. Confirm the support status is FULL.
4. Run `/spawnmob <newmob>`, `/spawnmob <mob>:<variant>`.
5. Run `/give @s <newitem>`, `/give @s minecraft:<newitem>`.
6. Run `/enchant <newenchant>`, `/potion <neweffect>`.
7. Place an EssentialsX sign on each new sign type.
8. Run `/tree <newtree>` if Bukkit has the `TreeType`.
9. Run `/kit` with the new armor. Confirm auto-equip.
10. Teleport onto each new damaging block. Confirm `/tp` refuses.
11. Repeat on Spigot for `ReflServerStateProvider`.
+1 -1
View File
@@ -80,7 +80,7 @@ Java version:
bug fix on. If this feature does not apply to a server, strike through the server software using ~~strikethrough~~. If you have tested on other
environments, add a new line with relevant details.
-->
- [ ] Most recent Paper version (1.XX.Y, git-Paper-BUILD)
- [ ] Most recent Paper version (XX.YY.Z, git-Paper-BUILD)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8
+1 -1
View File
@@ -80,7 +80,7 @@ Java version:
bug fix on. If this feature does not apply to a server, strike through the server software using ~~strikethrough~~. If you have tested on other
environments, add a new line with relevant details.
-->
- [ ] Most recent Paper version (1.XX.Y, git-Paper-BUILD)
- [ ] Most recent Paper version (XX.YY.Z, git-Paper-BUILD)
- [ ] CraftBukkit/Spigot/Paper 1.12.2
- [ ] CraftBukkit 1.8.8
+59 -5
View File
@@ -20,11 +20,11 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK 17
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
java-version: 25
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
@@ -41,11 +41,65 @@ jobs:
report_paths: '**/build/test-results/test*/TEST-*.xml'
- name: Archive plugin jars on GitHub
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: EssentialsX plugin jars
path: jars/
- name: Upload EssentialsX jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsX-*.jar
archive: false
- name: Upload EssentialsXAntiBuild jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXAntiBuild-*.jar
archive: false
- name: Upload EssentialsXChat jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXChat-*.jar
archive: false
- name: Upload EssentialsXDiscord jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXDiscord-*.jar
archive: false
- name: Upload EssentialsXDiscordLink jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXDiscordLink-*.jar
archive: false
- name: Upload EssentialsXGeoIP jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXGeoIP-*.jar
archive: false
- name: Upload EssentialsXProtect jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXProtect-*.jar
archive: false
- name: Upload EssentialsXSpawn jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXSpawn-*.jar
archive: false
- name: Upload EssentialsXXMPP jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXXMPP-*.jar
archive: false
- name: Deploy to Maven repo
if: ${{ success() && github.event_name == 'push' && github.repository == 'EssentialsX/Essentials' && github.ref == 'refs/heads/2.x' }}
env:
@@ -67,7 +121,7 @@ jobs:
cp -r EssentialsXMPP/build/docs/javadoc/ javadocs/EssentialsXMPP/
- name: Archive Javadocs
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: javadocs
path: javadocs/
@@ -85,7 +139,7 @@ jobs:
node-version: 22
- name: Download Javadocs
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: javadocs
path: javadocs/
+57 -3
View File
@@ -27,11 +27,11 @@ jobs:
with:
fetch-depth: 0
- name: Set up JDK 17
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
java-version: 25
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
@@ -48,7 +48,61 @@ jobs:
report_paths: '**/build/test-results/test*/TEST-*.xml'
- name: Archive plugin jars on GitHub
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: EssentialsX plugin jars
path: jars/
- name: Upload EssentialsX jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsX-*.jar
archive: false
- name: Upload EssentialsXAntiBuild jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXAntiBuild-*.jar
archive: false
- name: Upload EssentialsXChat jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXChat-*.jar
archive: false
- name: Upload EssentialsXDiscord jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXDiscord-*.jar
archive: false
- name: Upload EssentialsXDiscordLink jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXDiscordLink-*.jar
archive: false
- name: Upload EssentialsXGeoIP jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXGeoIP-*.jar
archive: false
- name: Upload EssentialsXProtect jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXProtect-*.jar
archive: false
- name: Upload EssentialsXSpawn jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXSpawn-*.jar
archive: false
- name: Upload EssentialsXXMPP jar
uses: actions/upload-artifact@v7
with:
path: jars/EssentialsXXMPP-*.jar
archive: false
+10 -3
View File
@@ -21,6 +21,7 @@ jobs:
steps:
- name: Mirror dump to a private Gist
id: mirror
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GIST_TOKEN }}
@@ -46,13 +47,19 @@ jobs:
public: false
});
const link = `${process.env.GIST_LINK_FMT}${gist.id}`;
core.setOutput('link', `${process.env.GIST_LINK_FMT}${gist.id}`);
core.setOutput('issue_number', issue.number);
- name: Comment gist link on issue
if: steps.mirror.outputs.link != ''
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `📋 Essentials Dump Backup → ${link}`
issue_number: ${{ steps.mirror.outputs.issue_number }},
body: `📋 Essentials Dump Backup → ${{ steps.mirror.outputs.link }}`
});
cleanup-gist:
+2 -2
View File
@@ -11,8 +11,8 @@
</option>
<option name="locations">
<list>
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">bundled-sun-checks</ConfigurationLocation>
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">bundled-google-checks</ConfigurationLocation>
<ConfigurationLocation id="321e5753-4201-4d2c-bf31-6232011122fd" type="PROJECT_RELATIVE" scope="All" description="EssentialsX">
$PROJECT_DIR$/.checkstyle/checkstyle.xml
<option name="properties">
+1 -3
View File
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
compileOnly('com.github.milkbowl:VaultAPI:1.7') {
compileOnly('net.essentialsx.deps:VaultAPI:1.7') {
exclude group: "org.bukkit", module: "bukkit"
}
compileOnly 'net.luckperms:api:5.3'
@@ -25,7 +25,6 @@ dependencies {
api(project(path: ':providers:PaperProvider', configuration: 'shadow')){
exclude(module: 'paper-api')
}
api project(':providers:FoliaProvider')
api(project(':providers:NMSReflectionProvider')) {
exclude(module: 'bukkit')
}
@@ -74,7 +73,6 @@ shadowJar {
include (dependency('net.kyori:adventure-text-minimessage'))
include (project(':providers:BaseProviders'))
include (project(path: ':providers:PaperProvider', configuration: 'shadow'))
include (project(':providers:FoliaProvider'))
include (project(':providers:NMSReflectionProvider'))
include (project(':providers:1_8Provider'))
include (project(':providers:1_12Provider'))
@@ -116,6 +116,8 @@ public class AlternativeCommandsHandler {
}
// return the first command that is not an alias
Command firstAlias = null;
final Iterator<WeakReference<Command>> iterator = commands.iterator();
while (iterator.hasNext()) {
final Command cmd = iterator.next().get();
@@ -127,10 +129,14 @@ public class AlternativeCommandsHandler {
if (cmd.getName().equalsIgnoreCase(label)) {
return cmd;
}
if (firstAlias == null) {
firstAlias = cmd;
}
}
// return the first alias
return commands.get(0).get();
// return the first alias, or null if every reference in this bucket has been collected
return firstAlias;
}
public void executed(final String label, final Command pc) {
@@ -139,6 +139,19 @@ public class AsyncTeleport implements IAsyncTeleport {
paperFuture.exceptionally(future::completeExceptionally);
}
private void runOnMain(final Runnable runnable) throws ExecutionException, InterruptedException {
if (Bukkit.isPrimaryThread()) {
runnable.run();
return;
}
final CompletableFuture<Object> taskLock = new CompletableFuture<>();
Bukkit.getScheduler().runTask(ess, () -> {
runnable.run();
taskLock.complete(new Object());
});
taskLock.get();
}
protected void nowAsync(final IUser teleportee, final ITarget target, final TeleportCause cause, final CompletableFuture<Boolean> future) {
cancel(false);
@@ -156,8 +169,8 @@ public class AsyncTeleport implements IAsyncTeleport {
}
try {
ess.ensureEntity(teleportee.getBase(), () -> teleportee.getBase().eject()); //EntityDismountEvent requires a sync context.
} catch (final RuntimeException e) {
runOnMain(() -> teleportee.getBase().eject()); //EntityDismountEvent requires a sync context.
} catch (final ExecutionException | InterruptedException e) {
future.completeExceptionally(e);
return;
}
@@ -177,7 +190,8 @@ public class AsyncTeleport implements IAsyncTeleport {
if (LocationUtil.isBlockUnsafeForUser(ess, teleportee, chunk.getWorld(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ())) {
if (ess.getSettings().isTeleportSafetyEnabled()) {
if (ess.getSettings().isForceDisableTeleportSafety()) {
PaperLib.teleportAsync(teleportee.getBase(), loc, cause);
//The chunk we're teleporting to is 100% going to be loaded here, no need to teleport async.
teleportee.getBase().teleport(loc, cause);
} else {
try {
//There's a chance the safer location is outside the loaded chunk so still teleport async here.
@@ -193,7 +207,8 @@ public class AsyncTeleport implements IAsyncTeleport {
}
} else {
if (ess.getSettings().isForceDisableTeleportSafety()) {
PaperLib.teleportAsync(teleportee.getBase(), loc, cause);
//The chunk we're teleporting to is 100% going to be loaded here, no need to teleport async.
teleportee.getBase().teleport(loc, cause);
} else {
if (ess.getSettings().isTeleportToCenterLocation()) {
loc = LocationUtil.getRoundedDestination(loc);
@@ -1,14 +1,13 @@
package com.earth2me.essentials;
import net.ess3.api.IEssentials;
import net.ess3.api.IUser;
import net.ess3.provider.SchedulingProvider;
import org.bukkit.Location;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.bukkit.Location;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import net.ess3.api.IEssentials;
import net.ess3.api.IUser;
import net.essentialsx.api.v2.events.TeleportWarmupCancelledEvent;
import net.essentialsx.api.v2.events.TeleportWarmupCancelledEvent.CancelReason;
@@ -32,7 +31,7 @@ public class AsyncTimedTeleport implements Runnable {
private final boolean timer_canMove;
private final Trade timer_chargeFor;
private final TeleportCause timer_cause;
private SchedulingProvider.EssentialsTask timer_task;
private int timer_task;
private double timer_health;
AsyncTimedTeleport(final IUser user, final IEssentials ess, final AsyncTeleport teleport, final long delay, final IUser teleportUser, final ITarget target, final Trade chargeFor, final TeleportCause cause, final boolean respawn) {
@@ -56,7 +55,7 @@ public class AsyncTimedTeleport implements Runnable {
this.timer_respawn = respawn;
this.timer_canMove = user.isAuthorized("essentials.teleport.timer.move");
timer_task = ess.runTaskTimerAsynchronously(this, 20, 20);
timer_task = ess.runTaskTimerAsynchronously(this, 20, 20).getTaskId();
if (future != null) {
this.parentFuture = future;
@@ -143,16 +142,16 @@ public class AsyncTimedTeleport implements Runnable {
}
}
ess.scheduleEntityDelayedTask(teleportOwner.getBase(), new DelayedTeleportTask());
ess.scheduleSyncDelayedTask(new DelayedTeleportTask());
}
//If we need to cancelTimer a pending teleportPlayer call this method
void cancelTimer(final boolean notifyUser) {
if (timer_task == null) {
if (timer_task == -1) {
return;
}
try {
timer_task.cancel();
ess.getServer().getScheduler().cancelTask(timer_task);
final IUser teleportUser = ess.getUser(this.timer_teleportee);
if (teleportUser != null && teleportUser.getBase() != null) {
@@ -168,7 +167,7 @@ public class AsyncTimedTeleport implements Runnable {
}
}
} finally {
timer_task = null;
timer_task = -1;
}
}
}
@@ -1,7 +1,6 @@
package com.earth2me.essentials;
import net.ess3.api.IEssentials;
import net.ess3.provider.SchedulingProvider;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
@@ -19,7 +18,7 @@ public class Backup implements Runnable {
private transient final IEssentials ess;
private final AtomicBoolean pendingShutdown = new AtomicBoolean(false);
private transient boolean running = false;
private transient SchedulingProvider.EssentialsTask task = null;
private transient int taskId = -1;
private transient boolean active = false;
private transient CompletableFuture<Object> taskLock = null;
@@ -37,10 +36,10 @@ public class Backup implements Runnable {
public synchronized void stopTask() {
running = false;
if (task != null) {
task.cancel();
if (taskId != -1) {
server.getScheduler().cancelTask(taskId);
}
task = null;
taskId = -1;
}
private synchronized void startTask() {
@@ -49,7 +48,7 @@ public class Backup implements Runnable {
if (interval < 1200) {
return;
}
task = ess.scheduleGlobalRepeatingTask(this, interval, interval);
taskId = ess.scheduleSyncRepeatingTask(this, interval, interval);
running = true;
}
}
@@ -124,7 +123,7 @@ public class Backup implements Runnable {
}
if (!pendingShutdown.get()) {
ess.scheduleGlobalDelayedTask(new BackupEnableSaveTask());
ess.scheduleSyncDelayedTask(new BackupEnableSaveTask());
}
}
});
@@ -13,7 +13,11 @@ import static com.earth2me.essentials.I18n.tlLiteral;
public final class Console implements IMessageRecipient {
public static final String NAME = "Console";
public static final String DISPLAY_NAME = tlLiteral("consoleName");
public static String displayName() {
return tlLiteral("consoleName");
}
private static Console instance; // Set in essentials
private final IEssentials ess;
@@ -56,7 +60,7 @@ public final class Console implements IMessageRecipient {
@Override
public String getDisplayName() {
return Console.DISPLAY_NAME;
return displayName();
}
@Override
@@ -17,13 +17,13 @@ public final class Enchantments {
static {
final Enchantment SHARPNESS = RegistryUtil.valueOf(Enchantment.class, "DAMAGE_ALL", "SHARPNESS");
ENCHANTMENTS.put("alldamage", SHARPNESS);
ALIASENCHANTMENTS.put("alldmg", SHARPNESS);
ENCHANTMENTS.put("sharpness", SHARPNESS);
ALIASENCHANTMENTS.put("sharp", SHARPNESS);
ALIASENCHANTMENTS.put("dal", SHARPNESS);
final Enchantment BANE_OF_ARTHROPODS = RegistryUtil.valueOf(Enchantment.class, "DAMAGE_ARTHROPODS", "BANE_OF_ARTHROPODS");
ENCHANTMENTS.put("ardmg", BANE_OF_ARTHROPODS);
@@ -31,13 +31,13 @@ public final class Enchantments {
ALIASENCHANTMENTS.put("baneofarthropod", BANE_OF_ARTHROPODS);
ALIASENCHANTMENTS.put("arthropod", BANE_OF_ARTHROPODS);
ALIASENCHANTMENTS.put("dar", BANE_OF_ARTHROPODS);
final Enchantment SMITE = RegistryUtil.valueOf(Enchantment.class, "DAMAGE_UNDEAD", "SMITE");
ENCHANTMENTS.put("undeaddamage", SMITE);
ENCHANTMENTS.put("smite", SMITE);
ALIASENCHANTMENTS.put("du", SMITE);
final Enchantment EFFICIENCY = RegistryUtil.valueOf(Enchantment.class, "DIG_SPEED", "EFFICIENCY");
ENCHANTMENTS.put("digspeed", EFFICIENCY);
@@ -46,7 +46,7 @@ public final class Enchantments {
ALIASENCHANTMENTS.put("cutspeed", EFFICIENCY);
ALIASENCHANTMENTS.put("ds", EFFICIENCY);
ALIASENCHANTMENTS.put("eff", EFFICIENCY);
final Enchantment UNBREAKING = RegistryUtil.valueOf(Enchantment.class, "DURABILITY", "UNBREAKING");
ENCHANTMENTS.put("durability", UNBREAKING);
@@ -70,21 +70,21 @@ public final class Enchantments {
ALIASENCHANTMENTS.put("kback", Enchantment.KNOCKBACK);
ALIASENCHANTMENTS.put("kb", Enchantment.KNOCKBACK);
ALIASENCHANTMENTS.put("k", Enchantment.KNOCKBACK);
final Enchantment FORTUNE = RegistryUtil.valueOf(Enchantment.class, "LOOT_BONUS_BLOCKS", "FORTUNE");
ALIASENCHANTMENTS.put("blockslootbonus", FORTUNE);
ENCHANTMENTS.put("fortune", FORTUNE);
ALIASENCHANTMENTS.put("fort", FORTUNE);
ALIASENCHANTMENTS.put("lbb", FORTUNE);
final Enchantment LOOTING = RegistryUtil.valueOf(Enchantment.class, "LOOT_BONUS_MOBS", "LOOTING");
ALIASENCHANTMENTS.put("mobslootbonus", LOOTING);
ENCHANTMENTS.put("mobloot", LOOTING);
ENCHANTMENTS.put("looting", LOOTING);
ALIASENCHANTMENTS.put("lbm", LOOTING);
final Enchantment RESPIRATION = RegistryUtil.valueOf(Enchantment.class, "OXYGEN", "RESPIRATION");
ALIASENCHANTMENTS.put("oxygen", RESPIRATION);
@@ -92,14 +92,14 @@ public final class Enchantments {
ALIASENCHANTMENTS.put("breathing", RESPIRATION);
ENCHANTMENTS.put("breath", RESPIRATION);
ALIASENCHANTMENTS.put("o", RESPIRATION);
final Enchantment PROTECTION = RegistryUtil.valueOf(Enchantment.class, "PROTECTION_ENVIRONMENTAL", "PROTECTION");
ENCHANTMENTS.put("protection", PROTECTION);
ALIASENCHANTMENTS.put("prot", PROTECTION);
ENCHANTMENTS.put("protect", PROTECTION);
ALIASENCHANTMENTS.put("p", PROTECTION);
final Enchantment BLAST_PROTECTION = RegistryUtil.valueOf(Enchantment.class, "PROTECTION_EXPLOSIONS", "BLAST_PROTECTION");
ALIASENCHANTMENTS.put("explosionsprotection", BLAST_PROTECTION);
@@ -110,7 +110,7 @@ public final class Enchantments {
ALIASENCHANTMENTS.put("bprotect", BLAST_PROTECTION);
ENCHANTMENTS.put("blastprotect", BLAST_PROTECTION);
ALIASENCHANTMENTS.put("pe", BLAST_PROTECTION);
final Enchantment FEATHER_FALLING = RegistryUtil.valueOf(Enchantment.class, "PROTECTION_FALL", "FEATHER_FALLING");
ALIASENCHANTMENTS.put("fallprotection", FEATHER_FALLING);
@@ -118,7 +118,7 @@ public final class Enchantments {
ENCHANTMENTS.put("featherfall", FEATHER_FALLING);
ALIASENCHANTMENTS.put("featherfalling", FEATHER_FALLING);
ALIASENCHANTMENTS.put("pfa", FEATHER_FALLING);
final Enchantment FIRE_PROTECTION = RegistryUtil.valueOf(Enchantment.class, "PROTECTION_FIRE", "FIRE_PROTECTION");
ALIASENCHANTMENTS.put("fireprotection", FIRE_PROTECTION);
@@ -128,7 +128,7 @@ public final class Enchantments {
ENCHANTMENTS.put("fireprot", FIRE_PROTECTION);
ALIASENCHANTMENTS.put("flameprot", FIRE_PROTECTION);
ALIASENCHANTMENTS.put("pf", FIRE_PROTECTION);
final Enchantment PROJECTILE_PROTECTION = RegistryUtil.valueOf(Enchantment.class, "PROTECTION_PROJECTILE", "PROJECTILE_PROTECTION");
ENCHANTMENTS.put("projectileprotection", PROJECTILE_PROTECTION);
@@ -138,28 +138,28 @@ public final class Enchantments {
ENCHANTMENTS.put("silktouch", Enchantment.SILK_TOUCH);
ALIASENCHANTMENTS.put("softtouch", Enchantment.SILK_TOUCH);
ALIASENCHANTMENTS.put("st", Enchantment.SILK_TOUCH);
final Enchantment AQUA_AFFINITY = RegistryUtil.valueOf(Enchantment.class, "WATER_WORKER", "AQUA_AFFINITY");
ENCHANTMENTS.put("waterworker", AQUA_AFFINITY);
ENCHANTMENTS.put("aquaaffinity", AQUA_AFFINITY);
ALIASENCHANTMENTS.put("watermine", AQUA_AFFINITY);
ALIASENCHANTMENTS.put("ww", AQUA_AFFINITY);
final Enchantment FLAME = RegistryUtil.valueOf(Enchantment.class, "ARROW_FIRE", "FLAME");
ALIASENCHANTMENTS.put("firearrow", FLAME);
ENCHANTMENTS.put("flame", FLAME);
ENCHANTMENTS.put("flamearrow", FLAME);
ALIASENCHANTMENTS.put("af", FLAME);
final Enchantment POWER = RegistryUtil.valueOf(Enchantment.class, "ARROW_DAMAGE", "POWER");
ENCHANTMENTS.put("arrowdamage", POWER);
ENCHANTMENTS.put("power", POWER);
ALIASENCHANTMENTS.put("arrowpower", POWER);
ALIASENCHANTMENTS.put("ad", POWER);
final Enchantment PUNCH = RegistryUtil.valueOf(Enchantment.class, "ARROW_KNOCKBACK", "PUNCH");
ENCHANTMENTS.put("arrowknockback", PUNCH);
@@ -167,7 +167,7 @@ public final class Enchantments {
ENCHANTMENTS.put("punch", PUNCH);
ALIASENCHANTMENTS.put("arrowpunch", PUNCH);
ALIASENCHANTMENTS.put("ak", PUNCH);
final Enchantment INFINITY = RegistryUtil.valueOf(Enchantment.class, "ARROW_INFINITE", "INFINITY");
ALIASENCHANTMENTS.put("infinitearrows", INFINITY);
@@ -177,7 +177,7 @@ public final class Enchantments {
ALIASENCHANTMENTS.put("unlimited", INFINITY);
ALIASENCHANTMENTS.put("unlimitedarrows", INFINITY);
ALIASENCHANTMENTS.put("ai", INFINITY);
final Enchantment LUCK_OF_THE_SEA = RegistryUtil.valueOf(Enchantment.class, "LUCK", "LUCK_OF_THE_SEA");
ENCHANTMENTS.put("luck", LUCK_OF_THE_SEA);
@@ -313,7 +313,7 @@ public final class Enchantments {
ENCHANTMENTS.put("density", density);
}
final Enchantment windBurst = Enchantment.getByName("WIND_BURST");
if (breach != null) {
if (windBurst != null) {
ENCHANTMENTS.put("windburst", windBurst);
ALIASENCHANTMENTS.put("wind", windBurst);
ALIASENCHANTMENTS.put("burst", windBurst);
@@ -321,6 +321,14 @@ public final class Enchantments {
} catch (final IllegalArgumentException ignored) {
}
try { // 1.21.11
final Enchantment lunge = Enchantment.getByName("LUNGE");
if (lunge != null) {
ENCHANTMENTS.put("lunge", lunge);
}
} catch (final IllegalArgumentException ignored) {
}
try {
final Class<?> namespacedKeyClass = Class.forName("org.bukkit.NamespacedKey");
final Class<?> enchantmentClass = Class.forName("org.bukkit.enchantments.Enchantment");
@@ -28,7 +28,6 @@ import com.earth2me.essentials.commands.NoChargeException;
import com.earth2me.essentials.commands.NotEnoughArgumentsException;
import com.earth2me.essentials.commands.PlayerNotFoundException;
import com.earth2me.essentials.commands.QuietAbortException;
import com.earth2me.essentials.config.EssentialsConfiguration;
import com.earth2me.essentials.economy.EconomyLayers;
import com.earth2me.essentials.economy.vault.VaultEconomyProvider;
import com.earth2me.essentials.items.AbstractItemDb;
@@ -47,9 +46,12 @@ import com.earth2me.essentials.textreader.SimpleTextInput;
import com.earth2me.essentials.updatecheck.UpdateChecker;
import com.earth2me.essentials.userstorage.ModernUserMap;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.ModernPaperEnvironment;
import com.earth2me.essentials.utils.PasteUtil;
import com.earth2me.essentials.utils.VersionUtil;
import io.papermc.lib.PaperLib;
import net.ess3.api.Economy;
import com.earth2me.essentials.config.EssentialsConfiguration;
import net.ess3.api.IEssentials;
import net.ess3.api.IItemDb;
import net.ess3.api.IJails;
@@ -68,21 +70,19 @@ import net.ess3.provider.InventoryViewProvider;
import net.ess3.provider.KnownCommandsProvider;
import net.ess3.provider.PlayerLocaleProvider;
import net.ess3.provider.ProviderListener;
import net.ess3.provider.SchedulingProvider;
import net.ess3.provider.ServerStateProvider;
import net.ess3.provider.providers.BaseBannerDataProvider;
import net.ess3.provider.providers.BaseInventoryViewProvider;
import net.ess3.provider.providers.BlockMetaSpawnerItemProvider;
import net.ess3.provider.providers.BukkitMaterialTagProvider;
import net.ess3.provider.providers.BukkitSchedulingProvider;
import net.ess3.provider.providers.BukkitSpawnerBlockProvider;
import net.ess3.provider.providers.BukkitTileEntityProvider;
import net.ess3.provider.providers.BukkitWorldTileEntityCountProvider;
import net.ess3.provider.providers.FixedHeightWorldInfoProvider;
import net.ess3.provider.providers.FlatSpawnEggProvider;
import net.ess3.provider.providers.FoliaSchedulingProvider;
import net.ess3.provider.providers.LegacyBannerDataProvider;
import net.ess3.provider.providers.LegacyBiomeNameProvider;
import net.ess3.provider.providers.LegacyPatternTypeProvider;
import net.ess3.provider.providers.ModernPatternTypeProvider;
import net.ess3.provider.providers.LegacyDamageEventProvider;
import net.ess3.provider.providers.LegacyInventoryViewProvider;
import net.ess3.provider.providers.LegacyItemUnbreakableProvider;
@@ -106,12 +106,10 @@ import net.ess3.provider.providers.PaperSerializationProvider;
import net.ess3.provider.providers.PaperServerStateProvider;
import net.ess3.provider.providers.PaperTickCountProvider;
import net.ess3.provider.providers.PaperTileEntityProvider;
import net.ess3.provider.providers.PaperWorldTileEntityCountProvider;
import net.ess3.provider.providers.PrehistoricPotionMetaProvider;
import net.essentialsx.api.v2.services.BalanceTop;
import net.essentialsx.api.v2.services.mail.MailService;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.block.Block;
@@ -121,7 +119,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.PluginIdentifiableCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
@@ -136,6 +133,8 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
import java.util.ArrayList;
import java.util.Collection;
@@ -148,11 +147,11 @@ import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.earth2me.essentials.I18n.tlLiteral;
import static com.earth2me.essentials.I18n.tlLocale;
@@ -263,6 +262,10 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
getLogger().severe(getAdventureFacet().miniToLegacy(tlLiteral("serverSnapshot")));
}
if (PaperLib.isPaper() && !PaperLib.isVersion(13) && VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_13_0_R01)) {
PaperLib.setCustomEnvironment(new ModernPaperEnvironment());
}
final PluginManager pm = getServer().getPluginManager();
for (final Plugin plugin : pm.getPlugins()) {
if (plugin.getDescription().getName().startsWith("Essentials") && !plugin.getDescription().getVersion().equals(this.getDescription().getVersion()) && !plugin.getDescription().getName().equals("EssentialsAntiCheat")) {
@@ -329,6 +332,9 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
confList.add(jails);
execTimer.mark("Init(Jails)");
EconomyLayers.onEnable(this);
execTimer.mark("Init(EconomyLayers)");
// Spawner item provider only uses one, but it's here for legacy...
providerFactory.registerProvider(BlockMetaSpawnerItemProvider.class);
@@ -344,6 +350,9 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
//Banner Meta Provider
providerFactory.registerProvider(LegacyBannerDataProvider.class, BaseBannerDataProvider.class);
// Pattern Type Provider
providerFactory.registerProvider(LegacyPatternTypeProvider.class, ModernPatternTypeProvider.class);
// Server State Provider
providerFactory.registerProvider(ReflServerStateProvider.class, PaperServerStateProvider.class);
@@ -398,16 +407,10 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
// Tile Entity Provider
providerFactory.registerProvider(BukkitTileEntityProvider.class, PaperTileEntityProvider.class);
// World Tile Entity Count Provider
providerFactory.registerProvider(BukkitWorldTileEntityCountProvider.class, PaperWorldTileEntityCountProvider.class);
// Tick Count Provider
providerFactory.registerProvider(PaperTickCountProvider.class);
// Scheduling provider
providerFactory.registerProvider(FoliaSchedulingProvider.class, BukkitSchedulingProvider.class);
if (!TESTING || true) { // FIXME: The scheduling provider is necessary for tests to pass without adding checks for TESTING everywhere
if (!TESTING) {
providerFactory.finalizeRegistration();
}
@@ -428,12 +431,6 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
}
execTimer.mark("Init(Providers)");
EconomyLayers.onEnable(this);
execTimer.mark("Init(EconomyLayers)");
registerListeners(getServer().getPluginManager());
initAdventureFacet();
reload();
// The item spawn blacklist is loaded with all other settings, before the item
@@ -444,7 +441,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
alternativeCommandsHandler = new AlternativeCommandsHandler(this);
timer = new EssentialsTimer(this);
scheduleGlobalRepeatingTask(timer, 1000, 50);
scheduleSyncRepeatingTask(timer, 1000, 50);
Economy.setEss(this);
execTimer.mark("RegHandler");
@@ -497,6 +494,8 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
}
private void registerListeners(final PluginManager pm) {
HandlerList.unregisterAll(this);
if (getSettings().isDebug()) {
LOGGER.log(Level.INFO, "Registering Listeners");
}
@@ -594,6 +593,8 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
getUsers().shutdown();
EssentialsConfiguration.shutdownExecutor();
PasteUtil.shutdownExecutor();
getServer().getScheduler().cancelTasks(this);
HandlerList.unregisterAll(this);
}
@@ -616,6 +617,9 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
}
}
final PluginManager pm = getServer().getPluginManager();
registerListeners(pm);
initAdventureFacet();
}
@@ -624,7 +628,9 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
adventureFacet.close();
}
if (VersionUtil.isPaper() && VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_16_5_R01)) {
// Paper only started bundling a modern enough native Adventure (with the MiniMessage TagResolver API)
// in 1.18.2, so older versions must continue using our bundled Adventure via the Spigot facet.
if (VersionUtil.isPaper() && VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_18_2_R01)) {
adventureFacet = new PaperAdventureFacet(getSettings() != null ? getSettings().getPrimaryColor() : null, getSettings() != null ? getSettings().getSecondaryColor() : null);
} else {
adventureFacet = new SpigotAdventureFacet(this);
@@ -848,7 +854,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
sender.sendTl("commandHelpLine2", description);
sender.sendTl("commandHelpLine3");
for (Map.Entry<String, String> usage : cmd.getUsageStrings().entrySet()) {
sender.sendTl("commandHelpLineUsage", AdventureUtil.parsed(usage.getKey().replace("<command>", commandLabel)), AdventureUtil.parsed(usage.getValue()));
sender.sendTl("commandHelpLineUsage", AdventureUtil.parsed(usage.getKey().replace("<command>", commandLabel)), AdventureUtil.parsed(sender.tl(usage.getValue())));
}
} else {
sender.sendMessage(command.getDescription());
@@ -909,6 +915,16 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
}
}
@Override
public BukkitScheduler getScheduler() {
return this.getServer().getScheduler();
}
@Override
public List<Player> getJailedPlayers() {
return getUsers().getAllUserUUIDs().stream().map(this::getUser).filter(Objects::nonNull).filter(User::isJailed).map(User::getBase).filter(Objects::nonNull).collect(Collectors.toList());
}
@Override
public IJails getJails() {
return jails;
@@ -1232,135 +1248,33 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
}
@Override
public void scheduleInitTask(Runnable runnable) {
provider(SchedulingProvider.class).registerInitTask(runnable);
public BukkitTask runTaskAsynchronously(final Runnable run) {
return this.getScheduler().runTaskAsynchronously(this, run);
}
@Override
public void runTaskAsynchronously(final Runnable run) {
provider(SchedulingProvider.class).runAsyncTask(run);
public BukkitTask runTaskLaterAsynchronously(final Runnable run, final long delay) {
return this.getScheduler().runTaskLaterAsynchronously(this, run, delay);
}
@Override
public void runTaskLaterAsynchronously(final Runnable run, final long delay) {
provider(SchedulingProvider.class).runAsyncTaskLater(run, delay);
public BukkitTask runTaskTimerAsynchronously(final Runnable run, final long delay, final long period) {
return this.getScheduler().runTaskTimerAsynchronously(this, run, delay, period);
}
@Override
public SchedulingProvider.EssentialsTask runTaskTimerAsynchronously(final Runnable run, final long delay, final long period) {
return provider(SchedulingProvider.class).runAsyncTaskRepeating(run, delay, period);
public int scheduleSyncDelayedTask(final Runnable run) {
return this.getScheduler().scheduleSyncDelayedTask(this, run);
}
@Override
public void scheduleEntityDelayedTask(Entity entity, Runnable run) {
provider(SchedulingProvider.class).runEntityTask(entity, run);
public int scheduleSyncDelayedTask(final Runnable run, final long delay) {
return this.getScheduler().scheduleSyncDelayedTask(this, run, delay);
}
@Override
public SchedulingProvider.EssentialsTask scheduleEntityDelayedTask(Entity entity, Runnable run, long delay) {
return provider(SchedulingProvider.class).runEntityTask(entity, run, delay);
}
@Override
public SchedulingProvider.EssentialsTask scheduleEntityRepeatingTask(Entity entity, Runnable run, long delay, long period) {
return provider(SchedulingProvider.class).runEntityTaskRepeating(entity, run, delay, period);
}
@Override
public void scheduleLocationDelayedTask(Location location, Runnable run) {
provider(SchedulingProvider.class).runLocationalTask(location, run);
}
@Override
public void scheduleLocationDelayedTask(Location location, Runnable run, long delay) {
provider(SchedulingProvider.class).runLocationalTask(location, run, delay);
}
@Override
public SchedulingProvider.EssentialsTask scheduleLocationRepeatingTask(Location location, Runnable run, long delay, long period) {
return provider(SchedulingProvider.class).runLocationalTaskRepeating(location, run, delay, period);
}
@Override
public void scheduleGlobalDelayedTask(Runnable run, long delay) {
provider(SchedulingProvider.class).runGlobalLocationalTask(run, delay);
}
@Override
public SchedulingProvider.EssentialsTask scheduleGlobalRepeatingTask(Runnable run, long delay, long period) {
return provider(SchedulingProvider.class).runGlobalLocationalTaskRepeating(run, delay, period);
}
@Override
public boolean isEntityThread(Entity entity) {
return provider(SchedulingProvider.class).isEntityThread(entity);
}
@Override
public boolean isRegionThread(Location location) {
return provider(SchedulingProvider.class).isRegionThread(location);
}
@Override
public boolean isGlobalThread() {
return provider(SchedulingProvider.class).isGlobalThread();
}
@Override
public void ensureEntity(Entity entity, Runnable runnable) {
if (isEntityThread(entity)) {
runnable.run();
return;
}
final CompletableFuture<Object> taskLock = new CompletableFuture<>();
scheduleEntityDelayedTask(entity, () -> {
runnable.run();
taskLock.complete(new Object());
});
try {
taskLock.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public void ensureRegion(Location location, Runnable runnable) {
if (isRegionThread(location)) {
runnable.run();
return;
}
final CompletableFuture<Object> taskLock = new CompletableFuture<>();
scheduleLocationDelayedTask(location, () -> {
runnable.run();
taskLock.complete(new Object());
});
try {
taskLock.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public void ensureGlobal(Runnable runnable) {
if (isGlobalThread()) {
runnable.run();
return;
}
final CompletableFuture<Object> taskLock = new CompletableFuture<>();
scheduleGlobalDelayedTask(() -> {
runnable.run();
taskLock.complete(new Object());
});
try {
taskLock.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
public int scheduleSyncRepeatingTask(final Runnable run, final long delay, final long period) {
return this.getScheduler().scheduleSyncRepeatingTask(this, run, delay, period);
}
@Override
@@ -44,7 +44,7 @@ public class EssentialsBlockListener implements Listener {
final User user = ess.getUser(event.getPlayer());
if (user.hasUnlimited(is) && user.getBase().getGameMode() == GameMode.SURVIVAL) {
ess.scheduleEntityDelayedTask(user.getBase(), () -> {
ess.scheduleSyncDelayedTask(() -> {
if (is != null && is.getType() != null && !MaterialUtil.isAir(is.getType())) {
final ItemStack cloneIs = is.clone();
cloneIs.setAmount(1);
@@ -110,7 +110,7 @@ public class EssentialsEntityListener implements Listener {
}
}
ess.scheduleEntityDelayedTask(attacker.getBase(), new PowerToolInteractTask());
ess.scheduleSyncDelayedTask(new PowerToolInteractTask());
event.setCancelled(true);
return;
@@ -22,7 +22,6 @@ import io.papermc.paper.event.player.PlayerServerFullCheckEvent;
import net.ess3.api.IEssentials;
import net.ess3.api.events.AfkStatusChangeEvent;
import net.ess3.provider.CommandSendListenerProvider;
import net.ess3.provider.SchedulingProvider;
import net.ess3.provider.FormattedCommandAliasProvider;
import net.ess3.provider.InventoryViewProvider;
import net.ess3.provider.KnownCommandsProvider;
@@ -95,7 +94,7 @@ import static com.earth2me.essentials.I18n.tlLiteral;
public class EssentialsPlayerListener implements Listener {
private final transient IEssentials ess;
private final ConcurrentHashMap<UUID, SchedulingProvider.EssentialsTask> pendingMotdTasks = new ConcurrentHashMap<>();
private final ConcurrentHashMap<UUID, Integer> pendingMotdTasks = new ConcurrentHashMap<>();
public EssentialsPlayerListener(final IEssentials parent) {
this.ess = parent;
@@ -194,22 +193,7 @@ public class EssentialsPlayerListener implements Listener {
final User user = ess.getUser(event.getPlayer());
if (user.isMuted()) {
event.setCancelled(true);
final String dateDiff = user.getMuteTimeout() > 0 ? DateUtil.formatDateDiff(user.getMuteTimeout()) : null;
if (dateDiff == null) {
if (user.hasMuteReason()) {
user.sendTl("voiceSilencedReason", user.getMuteReason());
} else {
user.sendTl("voiceSilenced");
}
} else {
if (user.hasMuteReason()) {
user.sendTl("voiceSilencedReasonTime", dateDiff, user.getMuteReason());
} else {
user.sendTl("voiceSilencedTime", dateDiff);
}
}
user.notifyMuted();
ess.getLogger().info(ess.getAdventureFacet().miniToLegacy(tlLiteral("mutedUserSpeaks", user.getName(), event.getMessage())));
}
try {
@@ -290,9 +274,9 @@ public class EssentialsPlayerListener implements Listener {
public void onPlayerQuit(final PlayerQuitEvent event) {
final User user = ess.getUser(event.getPlayer());
final SchedulingProvider.EssentialsTask pendingTask = pendingMotdTasks.remove(user.getUUID());
if (pendingTask != null) {
pendingTask.cancel();
final Integer pendingId = pendingMotdTasks.remove(user.getUUID());
if (pendingId != null) {
ess.getScheduler().cancelTask(pendingId);
}
if (hideJoinQuitMessages() || ess.getSettings().allowSilentJoinQuit() && user.isAuthorized("essentials.silentquit")) {
@@ -421,14 +405,25 @@ public class EssentialsPlayerListener implements Listener {
final String lastAccountName = user.getLastAccountName(); // For comparison
user.setLastAccountName(user.getBase().getName());
final boolean newUsername = lastAccountName != null && !lastAccountName.equals(user.getBase().getName());
// A null last account name means EssentialsX has never recorded this player before, i.e. it's their first join.
// We rely on EssentialsX's own user data here rather than Player#hasPlayedBefore(), which is unreliable on modern
// server platforms that persist player data during the login/configuration phase.
// See https://github.com/EssentialsX/Essentials/issues/6466, https://github.com/EssentialsX/Essentials/issues/6464
final boolean firstJoin = lastAccountName == null;
// If the Minecraft account name changed, reset the nickname so the old one doesn't persist
if (ess.getSettings().isResetNickOnNameChange() && newUsername && user.getNickname() != null) {
user.setNickname(null);
}
user.setLastLogin(currentTime);
user.setDisplayNick();
updateCompass(user);
user.setLeavingHidden(false);
// Check for new username. If they don't want the message, let's just say it's false.
final boolean newUsername = ess.getSettings().isCustomNewUsernameMessage() && lastAccountName != null && !lastAccountName.equals(user.getBase().getName());
if (!ess.getVanishedPlayersNew().isEmpty() && !user.isAuthorized("essentials.vanish.see")) {
for (final String p : ess.getVanishedPlayersNew()) {
final Player toVanish = ess.getServer().getPlayerExact(p);
@@ -454,7 +449,7 @@ public class EssentialsPlayerListener implements Listener {
} else if (message == null || hideJoinQuitMessages()) {
effectiveMessage = null;
} else if (ess.getSettings().isCustomJoinMessage()) {
final String msg = (newUsername ? ess.getSettings().getCustomNewUsernameMessage() : ess.getSettings().getCustomJoinMessage())
final String msg = (newUsername && ess.getSettings().isCustomNewUsernameMessage() ? ess.getSettings().getCustomNewUsernameMessage() : ess.getSettings().getCustomJoinMessage())
.replace("{PLAYER}", user.getDisplayName()).replace("{USERNAME}", user.getName())
.replace("{UNIQUE}", NumberFormat.getInstance().format(ess.getUsers().getUserCount()))
.replace("{ONLINE}", NumberFormat.getInstance().format(ess.getOnlinePlayers().size()))
@@ -474,13 +469,13 @@ public class EssentialsPlayerListener implements Listener {
joinMessageConsumer.accept(effectiveMessage);
}
ess.runTaskAsynchronously(() -> ess.getServer().getPluginManager().callEvent(new AsyncUserDataLoadEvent(user, effectiveMessage)));
ess.runTaskAsynchronously(() -> ess.getServer().getPluginManager().callEvent(new AsyncUserDataLoadEvent(user, effectiveMessage, firstJoin)));
if (ess.getSettings().getMotdDelay() >= 0) {
final int motdDelay = ess.getSettings().getMotdDelay() / 50;
final Runnable motdTask = () -> motdFlow(user);
if (motdDelay > 0) {
pendingMotdTasks.put(user.getUUID(), ess.scheduleEntityDelayedTask(user.getBase(), motdTask, motdDelay));
pendingMotdTasks.put(user.getUUID(), ess.scheduleSyncDelayedTask(motdTask, motdDelay));
} else {
motdTask.run();
}
@@ -504,12 +499,21 @@ public class EssentialsPlayerListener implements Listener {
});
}
final boolean restoreFly = user.isFlyModeEnabled() && user.isAuthorized("essentials.fly");
if (restoreFly) {
user.getBase().setAllowFlight(true);
if (ess.getSettings().isSendFlyEnableOnJoin()) {
user.sendTl("flyMode", CommonPlaceholders.enableDisable(user.getSource(), true), user.getDisplayName());
}
}
if (user.isAuthorized("essentials.fly.safelogin")) {
user.getBase().setFallDistance(0);
if (LocationUtil.shouldFly(ess, user.getLocation())) {
final boolean inWater = VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_17_R01) && user.getBase().isInWater();
if (!inWater && LocationUtil.shouldFly(ess, user.getLocation())) {
user.getBase().setAllowFlight(true);
user.getBase().setFlying(true);
if (ess.getSettings().isSendFlyEnableOnJoin()) {
if (!restoreFly && ess.getSettings().isSendFlyEnableOnJoin()) {
user.sendTl("flyMode", CommonPlaceholders.enableDisable(user.getSource(), true), user.getDisplayName());
}
}
@@ -525,6 +529,11 @@ public class EssentialsPlayerListener implements Listener {
ess.getLogger().log(Level.INFO, "Set socialspy to false for {0} because they had it enabled without permission.", user.getName());
}
if (user.isFlyModeEnabled() && !user.isAuthorized("essentials.fly")) {
user.setFlyModeEnabled(false);
ess.getLogger().log(Level.INFO, "Set fly mode to false for {0} because they had it enabled without permission.", user.getName());
}
if (user.isGodModeEnabled() && !user.isAuthorized("essentials.god")) {
user.setGodModeEnabled(false);
ess.getLogger().log(Level.INFO, "Set god mode to false for {0} because they had it enabled without permission.", user.getName());
@@ -582,7 +591,7 @@ public class EssentialsPlayerListener implements Listener {
dUser.updateActivity(false, AfkStatusChangeEvent.Cause.JOIN);
dUser.stopTransaction();
ess.scheduleEntityDelayedTask(player, () -> {
ess.scheduleSyncDelayedTask(() -> {
final User user = ess.getUser(player);
if (!user.getBase().isOnline()) {
@@ -752,7 +761,7 @@ public class EssentialsPlayerListener implements Listener {
final User user = ess.getUser(event.getPlayer());
if (user.hasUnlimited(new ItemStack(event.getBucket()))) {
event.getItemStack().setType(event.getBucket());
ess.scheduleEntityDelayedTask(user.getBase(), user.getBase()::updateInventory);
ess.scheduleSyncDelayedTask(user.getBase()::updateInventory);
}
}
@@ -795,10 +804,9 @@ public class EssentialsPlayerListener implements Listener {
final ComponentHolder base = (user.isMuted() && ess.getSettings().getSocialSpyListenMutedPlayers())
? spyer.tlComponent("socialSpyMutedPrefix")
: spyer.tlComponent("socialSpyPrefix");
final ComponentHolder nameComponent = ess.getAdventureFacet().legacyToAdventure(playerName);
final ComponentHolder messageComponent = ess.getAdventureFacet().text(": " + event.getMessage());
final ComponentHolder formatted = ess.getAdventureFacet().deserializeMiniMessage(spyer.playerTl("socialSpyCmdFormat", playerName, event.getMessage()));
spyer.sendComponent(ess.getAdventureFacet().append(base, nameComponent, messageComponent));
spyer.sendComponent(ess.getAdventureFacet().append(base, formatted));
}
}
}
@@ -808,20 +816,7 @@ public class EssentialsPlayerListener implements Listener {
final User user = ess.getUser(player);
if (user.isMuted() && (ess.getSettings().getMuteCommands().contains(cmd) || ess.getSettings().getMuteCommands().contains("*"))) {
event.setCancelled(true);
final String dateDiff = user.getMuteTimeout() > 0 ? DateUtil.formatDateDiff(user.getMuteTimeout()) : null;
if (dateDiff == null) {
if (user.hasMuteReason()) {
user.sendTl("voiceSilencedReason", user.getMuteReason());
} else {
user.sendTl("voiceSilenced");
}
} else {
if (user.hasMuteReason()) {
user.sendTl("voiceSilencedReasonTime", dateDiff, user.getMuteReason());
} else {
user.sendTl("voiceSilencedTime", dateDiff);
}
}
user.notifyMuted();
ess.getLogger().info(ess.getAdventureFacet().miniToLegacy(tlLiteral("mutedUserSpeaks", player.getName(), event.getMessage())));
return;
}
@@ -1013,7 +1008,7 @@ public class EssentialsPlayerListener implements Listener {
}
}
ess.scheduleEntityDelayedTask(user.getBase(), new DelayedClickJumpTask());
ess.scheduleSyncDelayedTask(new DelayedClickJumpTask());
} catch (final Exception ex) {
if (ess.getSettings().isDebug()) {
ess.getLogger().log(Level.WARNING, ex.getMessage(), ex);
@@ -1044,7 +1039,7 @@ public class EssentialsPlayerListener implements Listener {
}
}
ess.scheduleEntityDelayedTask(user.getBase(), new PowerToolUseTask());
ess.scheduleSyncDelayedTask(new PowerToolUseTask());
}
}
@@ -1106,7 +1101,7 @@ public class EssentialsPlayerListener implements Listener {
}
if (refreshPlayer != null) {
ess.scheduleEntityDelayedTask(refreshPlayer, refreshPlayer::updateInventory, 1);
ess.scheduleSyncDelayedTask(refreshPlayer::updateInventory, 1);
}
}
@@ -1169,12 +1164,16 @@ public class EssentialsPlayerListener implements Listener {
}
if (refreshPlayer != null) {
ess.scheduleEntityDelayedTask(refreshPlayer, refreshPlayer::updateInventory, 1);
ess.scheduleSyncDelayedTask(refreshPlayer::updateInventory, 1);
}
}
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onPlayerFishEvent(final PlayerFishEvent event) {
if (!ess.getSettings().cancelAfkOnFish()) {
return;
}
final User user = ess.getUser(event.getPlayer());
user.updateActivityOnInteract(true);
}
@@ -1193,7 +1192,7 @@ public class EssentialsPlayerListener implements Listener {
final Player player = event.getPlayer();
if (player.isFlying() && player.getAllowFlight() && user.isAuthorized("essentials.fly")) {
// The gamemode change happens after the event, so we need to delay the flight enable
ess.scheduleEntityDelayedTask(player, () -> {
ess.scheduleSyncDelayedTask(() -> {
player.setAllowFlight(true);
player.setFlying(true);
}, 1);
@@ -1214,10 +1213,12 @@ public class EssentialsPlayerListener implements Listener {
public void onPlayerPickupItem(final org.bukkit.event.player.PlayerPickupItemEvent event) {
if (event.getItem().hasMetadata(Commandfireball.FIREBALL_META_KEY)) {
event.setCancelled(true);
} else if (ess.getSettings().getDisableItemPickupWhileAfk()) {
if (ess.getUser(event.getPlayer()).isAfk()) {
event.setCancelled(true);
}
return;
}
final User user = ess.getUser(event.getPlayer());
if ((ess.getSettings().getDisableItemPickupWhileAfk() && user.isAfk())
|| (user.isVanished() && !user.isAuthorizedCached("essentials.vanish.pickup"))) {
event.setCancelled(true);
}
}
}
@@ -1225,8 +1226,10 @@ public class EssentialsPlayerListener implements Listener {
private final class PickupListener1_12 implements Listener {
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onPlayerPickupItem(final org.bukkit.event.entity.EntityPickupItemEvent event) {
if (ess.getSettings().getDisableItemPickupWhileAfk() && event.getEntity() instanceof Player) {
if (ess.getUser((Player) event.getEntity()).isAfk()) {
if (event.getEntity() instanceof Player) {
final User user = ess.getUser((Player) event.getEntity());
if ((ess.getSettings().getDisableItemPickupWhileAfk() && user.isAfk())
|| (user.isVanished() && !user.isAuthorizedCached("essentials.vanish.pickup"))) {
event.setCancelled(true);
}
}
@@ -1245,6 +1248,10 @@ public class EssentialsPlayerListener implements Listener {
private final class CommandSendFilter implements CommandSendListenerProvider.Filter {
@Override
public Predicate<String> apply(Player player) {
// There is no event for op status changes, but the command list is resent when
// a player is opped/deopped, so we invalidate cached permissions here.
ess.getPermissionsHandler().invalidatePermissionCache(player.getUniqueId());
final User user = ess.getUser(player);
final Set<PluginCommand> checked = new HashSet<>();
final Set<PluginCommand> toRemove = new HashSet<>();
@@ -5,6 +5,7 @@ import org.bukkit.entity.Player;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
@@ -12,19 +13,36 @@ import java.util.logging.Level;
public class EssentialsTimer implements Runnable {
private final transient IEssentials ess;
private final transient Set<UUID> onlineUsers = new HashSet<>(); // Field is necessary for hidden users
private static final long maxTime = 10 * 1000000;
private final LinkedList<Double> history = new LinkedList<>();
@SuppressWarnings("FieldCanBeLocal")
private final long maxTime = 10 * 1000000;
@SuppressWarnings("FieldCanBeLocal")
private final long tickInterval = 50;
private transient long lastPoll = System.nanoTime();
private int skip1 = 0;
private int skip2 = 0;
EssentialsTimer(final IEssentials ess) {
this.ess = ess;
history.add(20d);
}
@Override
public void run() {
final long startTime = System.nanoTime();
final long currentTime = System.currentTimeMillis();
long timeSpent = (startTime - lastPoll) / 1000;
if (timeSpent == 0) {
timeSpent = 1;
}
if (history.size() > 10) {
history.remove();
}
final double tps = tickInterval * 1000000.0 / timeSpent;
if (tps <= 21) {
history.add(tps);
}
lastPoll = startTime;
int count = 0;
onlineUsers.clear();
for (final Player player : ess.getOnlinePlayers()) {
@@ -81,4 +99,14 @@ public class EssentialsTimer implements Runnable {
user.resetInvulnerabilityAfterTeleport();
}
}
public double getAverageTPS() {
double avg = 0;
for (final Double f : history) {
if (f != null) {
avg += f;
}
}
return avg / history.size();
}
}
@@ -28,6 +28,7 @@ import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.regex.Pattern;
@@ -35,7 +36,7 @@ import java.util.regex.Pattern;
public class I18n implements net.ess3.api.II18n {
private static final String MESSAGES = "messages";
private static final Pattern NODOUBLEMARK = Pattern.compile("''");
private static final ExecutorService BUNDLE_LOADER_EXECUTOR = Executors.newFixedThreadPool(2);
private static volatile ExecutorService BUNDLE_LOADER_EXECUTOR = Executors.newFixedThreadPool(2);
private static final ResourceBundle NULL_BUNDLE = new ResourceBundle() {
@SuppressWarnings("NullableProblems")
public Enumeration<String> getKeys() {
@@ -106,6 +107,29 @@ public class I18n implements net.ess3.api.II18n {
public void onDisable() {
instance = null;
shutdownExecutor();
}
private static synchronized ExecutorService getExecutor() {
if (BUNDLE_LOADER_EXECUTOR == null || BUNDLE_LOADER_EXECUTOR.isShutdown() || BUNDLE_LOADER_EXECUTOR.isTerminated()) {
BUNDLE_LOADER_EXECUTOR = Executors.newFixedThreadPool(2);
}
return BUNDLE_LOADER_EXECUTOR;
}
private static synchronized void shutdownExecutor() {
final ExecutorService exec = BUNDLE_LOADER_EXECUTOR;
if (exec == null) {
return;
}
exec.shutdown();
try {
if (!exec.awaitTermination(5, TimeUnit.SECONDS)) {
exec.shutdownNow();
}
} catch (final InterruptedException ignored) {
exec.shutdownNow();
}
}
@Override
@@ -124,7 +148,7 @@ public class I18n implements net.ess3.api.II18n {
synchronized (loadingBundles) {
if (!loadingBundles.contains(locale)) {
loadingBundles.add(locale);
BUNDLE_LOADER_EXECUTOR.submit(() -> {
getExecutor().submit(() -> {
blockingLoadBundle(locale);
synchronized (loadingBundles) {
loadingBundles.remove(locale);
@@ -9,19 +9,18 @@ import com.earth2me.essentials.commands.PlayerNotFoundException;
import com.earth2me.essentials.perm.PermissionsHandler;
import com.earth2me.essentials.updatecheck.UpdateChecker;
import com.earth2me.essentials.userstorage.IUserMap;
import net.ess3.provider.SchedulingProvider;
import net.ess3.provider.Provider;
import net.essentialsx.api.v2.services.BalanceTop;
import net.essentialsx.api.v2.services.mail.MailService;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
import java.util.Collection;
import java.util.List;
@@ -81,6 +80,10 @@ public interface IEssentials extends Plugin {
ISettings getSettings();
BukkitScheduler getScheduler();
List<Player> getJailedPlayers();
IJails getJails();
IWarps getWarps();
@@ -95,45 +98,17 @@ public interface IEssentials extends Plugin {
UpdateChecker getUpdateChecker();
void runTaskAsynchronously(Runnable run);
BukkitTask runTaskAsynchronously(Runnable run);
void runTaskLaterAsynchronously(Runnable run, long delay);
BukkitTask runTaskLaterAsynchronously(Runnable run, long delay);
SchedulingProvider.EssentialsTask runTaskTimerAsynchronously(Runnable run, long delay, long period);
BukkitTask runTaskTimerAsynchronously(Runnable run, long delay, long period);
void scheduleEntityDelayedTask(Entity entity, Runnable run);
int scheduleSyncDelayedTask(Runnable run);
SchedulingProvider.EssentialsTask scheduleEntityDelayedTask(Entity entity, Runnable run, long delay);
int scheduleSyncDelayedTask(Runnable run, long delay);
SchedulingProvider.EssentialsTask scheduleEntityRepeatingTask(Entity entity, Runnable run, long delay, long period);
void scheduleLocationDelayedTask(Location location, Runnable run);
void scheduleLocationDelayedTask(Location location, Runnable run, long delay);
SchedulingProvider.EssentialsTask scheduleLocationRepeatingTask(Location location, Runnable run, long delay, long period);
default void scheduleGlobalDelayedTask(Runnable run) {
scheduleGlobalDelayedTask(run, 1);
}
void scheduleGlobalDelayedTask(Runnable run, long delay);
SchedulingProvider.EssentialsTask scheduleGlobalRepeatingTask(Runnable run, long delay, long period);
void scheduleInitTask(Runnable runnable);
boolean isEntityThread(Entity entity);
boolean isRegionThread(Location location);
boolean isGlobalThread();
void ensureEntity(Entity entity, Runnable runnable);
void ensureRegion(Location location, Runnable runnable);
void ensureGlobal(Runnable runnable);
int scheduleSyncRepeatingTask(Runnable run, long delay, long period);
PermissionsHandler getPermissionsHandler();
@@ -94,6 +94,8 @@ public interface ISettings extends IConf {
String getNicknamePrefix();
boolean isResetNickOnNameChange();
String getOperatorColor() throws Exception;
boolean getPerWarpPermission();
@@ -134,6 +136,8 @@ public interface ISettings extends IConf {
boolean isAlwaysTeleportSafety();
boolean isConsiderWorldHeightForTeleportSafety();
boolean isTeleportPassengerDismount();
boolean isForcePassengerTeleport();
@@ -221,6 +225,8 @@ public interface ISettings extends IConf {
boolean cancelAfkOnChat();
boolean cancelAfkOnFish();
boolean sleepIgnoresAfkPlayers();
boolean sleepIgnoresVanishedPlayers();
@@ -32,6 +32,8 @@ import java.util.regex.Pattern;
public interface IUser {
boolean isAuthorized(String node);
boolean isAuthorizedCached(String node);
boolean isAuthorized(IEssentialsCommand cmd);
boolean isAuthorized(IEssentialsCommand cmd, String permissionPrefix);
@@ -13,6 +13,7 @@ import net.ess3.api.IEssentials;
import net.ess3.api.TranslatableException;
import net.ess3.provider.BannerDataProvider;
import net.ess3.provider.ItemUnbreakableProvider;
import net.ess3.provider.PatternTypeProvider;
import net.ess3.provider.PotionMetaProvider;
import org.bukkit.Color;
import org.bukkit.DyeColor;
@@ -207,8 +208,13 @@ public class MetaItemStack {
try {
final String components = Joiner.on(' ').join(Arrays.asList(string).subList(fromArg, string.length));
// modifyItemStack requires that the item namespaced key is prepended to the components for some reason
stack = ess.getServer().getUnsafe().modifyItemStack(stack, stack.getType().getKey() + components);
// From 1.20.6 through 1.21.11, modifyItemStack parses its argument as a full item string, so the item's
// namespaced key must be prepended to the components. As of 26.1, the implementation prepends the item
// key itself, so prepending it here would produce a malformed (double-namespaced) item string.
final String itemString = VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v26_1_R01)
? components
: stack.getType().getKey() + components;
stack = ess.getServer().getUnsafe().modifyItemStack(stack, itemString);
} catch (final NullPointerException npe) {
if (ess.getSettings().isDebug()) {
ess.getLogger().log(Level.INFO, "Itemstack is invalid", npe);
@@ -308,8 +314,14 @@ public class MetaItemStack {
final BookMeta meta = (BookMeta) stack.getItemMeta();
meta.setTitle(title);
stack.setItemMeta(meta);
} else if (split.length > 1 && split[0].startsWith("page") && split[0].length() > 4 && MaterialUtil.isEditableBook(stack.getType()) && hasMetaPermission(sender, "page", false, true, ess)) {
final int page = NumberUtil.isInt(split[0].substring(4)) ? (Integer.parseInt(split[0].substring(4)) - 1) : 0;
} else if (split.length > 1 && split[0].startsWith("page") && split[0].length() > 4
&& MaterialUtil.isEditableBook(stack.getType())
&& hasMetaPermission(sender, "page", false, true, ess)) {
final int page = NumberUtil.isInt(split[0].substring(4)) ? (Integer.parseInt(split[0].substring(4)) - 1)
: 0;
if (page > 100) {
throw new TranslatableException("pageLimitExceeded");
}
final BookMeta meta = (BookMeta) stack.getItemMeta();
final List<String> pages = meta.hasPages() ? new ArrayList<>(meta.getPages()) : new ArrayList<>();
final List<String> lines = new ArrayList<>();
@@ -706,22 +718,15 @@ public class MetaItemStack {
throw new TranslatableException("invalidBanner", split[1]);
}
PatternType patternType = null;
try {
//noinspection removal
patternType = PatternType.getByIdentifier(split[0]);
} catch (final Exception ignored) {
}
final PatternType patternType = ess.provider(PatternTypeProvider.class).getPatternTypeByIdentifier(split[0]);
final BannerMeta meta = (BannerMeta) stack.getItemMeta();
if (split[0].equalsIgnoreCase("basecolor")) {
final Color color = Color.fromRGB(Integer.parseInt(split[1]));
ess.provider(BannerDataProvider.class).setBaseColor(stack, DyeColor.getByColor(color));
} else if (patternType != null) {
//noinspection removal
final PatternType type = PatternType.getByIdentifier(split[0]);
final DyeColor color = DyeColor.getByColor(Color.fromRGB(Integer.parseInt(split[1])));
final org.bukkit.block.banner.Pattern pattern = new org.bukkit.block.banner.Pattern(color, type);
final org.bukkit.block.banner.Pattern pattern = new org.bukkit.block.banner.Pattern(color, patternType);
meta.addPattern(pattern);
}
@@ -733,12 +738,7 @@ public class MetaItemStack {
throw new TranslatableException("invalidBanner", split[1]);
}
PatternType patternType = null;
try {
//noinspection removal
patternType = PatternType.getByIdentifier(split[0]);
} catch (final Exception ignored) {
}
final PatternType patternType = ess.provider(PatternTypeProvider.class).getPatternTypeByIdentifier(split[0]);
// Hacky fix for accessing Shield meta - https://github.com/drtshock/Essentials/pull/745#issuecomment-234843795
final BlockStateMeta meta = (BlockStateMeta) stack.getItemMeta();
@@ -747,10 +747,8 @@ public class MetaItemStack {
final Color color = Color.fromRGB(Integer.parseInt(split[1]));
banner.setBaseColor(DyeColor.getByColor(color));
} else if (patternType != null) {
//noinspection removal
final PatternType type = PatternType.getByIdentifier(split[0]);
final DyeColor color = DyeColor.getByColor(Color.fromRGB(Integer.parseInt(split[1])));
final org.bukkit.block.banner.Pattern pattern = new org.bukkit.block.banner.Pattern(color, type);
final org.bukkit.block.banner.Pattern pattern = new org.bukkit.block.banner.Pattern(color, patternType);
banner.addPattern(pattern);
}
banner.update();
@@ -60,6 +60,7 @@ public enum Mob {
CHERRY_BOAT("CherryBoat", Enemies.NEUTRAL, "CHERRY_BOAT"),
BAMBOO_RAFT("BambooRaft", Enemies.NEUTRAL, "BAMBOO_RAFT"),
PALE_OAK_BOAT("PaleOakBoat", Enemies.NEUTRAL, "PALE_OAK_BOAT"),
POPLAR_BOAT("PoplarBoat", Enemies.NEUTRAL, "POPLAR_BOAT"),
MINECART("Minecart", Enemies.NEUTRAL, EntityType.MINECART),
MINECART_CHEST("ChestMinecart", Enemies.NEUTRAL, MobCompat.CHEST_MINECART),
MINECART_FURNACE("FurnaceMinecart", Enemies.NEUTRAL, MobCompat.FURNACE_MINECART),
@@ -118,7 +119,17 @@ public enum Mob {
FROG("Frog", Enemies.FRIENDLY, "FROG"),
TADPOLE("Tadpole", Enemies.FRIENDLY, "TADPOLE"),
WARDEN("Warden", Enemies.ENEMY, "WARDEN"),
CHEST_BOAT("ChestBoat", Enemies.NEUTRAL, "CHEST_BOAT"),
CHEST_BOAT("ChestBoat", Enemies.NEUTRAL, MobCompat.OAK_CHEST_BOAT),
ACACIA_CHEST_BOAT("AcaciaChestBoat", Enemies.NEUTRAL, "ACACIA_CHEST_BOAT"),
DARK_OAK_CHEST_BOAT("DarkOakChestBoat", Enemies.NEUTRAL, "DARK_OAK_CHEST_BOAT"),
BIRCH_CHEST_BOAT("BirchChestBoat", Enemies.NEUTRAL, "BIRCH_CHEST_BOAT"),
JUNGLE_CHEST_BOAT("JungleChestBoat", Enemies.NEUTRAL, "JUNGLE_CHEST_BOAT"),
SPRUCE_CHEST_BOAT("SpruceChestBoat", Enemies.NEUTRAL, "SPRUCE_CHEST_BOAT"),
MANGROVE_CHEST_BOAT("MangroveChestBoat", Enemies.NEUTRAL, "MANGROVE_CHEST_BOAT"),
CHERRY_CHEST_BOAT("CherryChestBoat", Enemies.NEUTRAL, "CHERRY_CHEST_BOAT"),
BAMBOO_CHEST_RAFT("BambooChestRaft", Enemies.NEUTRAL, "BAMBOO_CHEST_RAFT"),
PALE_OAK_CHEST_BOAT("PaleOakChestBoat", Enemies.NEUTRAL, "PALE_OAK_CHEST_BOAT"),
POPLAR_CHEST_BOAT("PoplarChestBoat", Enemies.NEUTRAL, "POPLAR_CHEST_BOAT"),
CAMEL("Camel", Enemies.FRIENDLY, "CAMEL"),
SNIFFER("Sniffer", Enemies.FRIENDLY, "SNIFFER"),
ARMADILLO("Armadillo", Enemies.FRIENDLY, "ARMADILLO"),
@@ -131,6 +142,8 @@ public enum Mob {
NAUTILUS("Nautilus", Enemies.NEUTRAL, "NAUTILUS"),
ZOMBIE_NAUTILUS("ZombieNautilus", Enemies.NEUTRAL, "ZOMBIE_NAUTILUS"),
PARCHED("Parched", Enemies.ENEMY, "PARCHED"),
SULFUR_CUBE("SulfurCube", Enemies.FRIENDLY, "SULFUR_CUBE"),
CUSHION("Cushion", Enemies.FRIENDLY, "CUSHION")
;
private static final Map<String, Mob> hashMap = new HashMap<>();
@@ -88,6 +88,7 @@ public final class MobCompat {
public static final EntityType END_CRYSTAL = getEntityType("END_CRYSTAL", "ENDER_CRYSTAL");
public static final EntityType FIREWORK_ROCKET = getEntityType("FIREWORK_ROCKET", "FIREWORK");
public static final EntityType OAK_BOAT = getEntityType("BOAT", "OAK_BOAT");
public static final EntityType OAK_CHEST_BOAT = getEntityType("CHEST_BOAT", "OAK_CHEST_BOAT");
private MobCompat() {
}
@@ -409,6 +410,7 @@ public final class MobCompat {
OAK("GENERIC", "OAK"),
SPRUCE("REDWOOD", "SPRUCE"),
MANGROVE("GENERIC", "MANGROVE"),
POPLAR("GENERIC", "POPLAR"),
;
private final String treeSpecies;
@@ -55,8 +55,8 @@ public enum MobData {
BABY_VILLAGER("child", EntityType.VILLAGER, Data.BABY, false),
TAMED_TAMEABLE("tamed", Tameable.class, Data.TAMED, true),
TAME_TAMEABLE("tame", Tameable.class, Data.TAMED, false),
RANDOM_SHEEP("random", EntityType.SHEEP, Data.COLORABLE, true),
COLORABLE_SHEEP("", Arrays.stream(DyeColor.values()).map(color -> color.name().toLowerCase(Locale.ENGLISH)).collect(Collectors.toList()), EntityType.SHEEP, Data.COLORABLE, true),
RANDOM_COLORABLE("random", Colorable.class, Data.COLORABLE, true),
COLORABLE("", Arrays.stream(DyeColor.values()).map(color -> color.name().toLowerCase(Locale.ENGLISH)).collect(Collectors.toList()), Colorable.class, Data.COLORABLE, true),
POLKA_HORSE("polka", EntityType.HORSE, Horse.Style.BLACK_DOTS, true),
SOOTY_HORSE("sooty", EntityType.HORSE, Horse.Style.BLACK_DOTS, false),
BLAZE_HORSE("blaze", EntityType.HORSE, Horse.Style.WHITE, true),
@@ -181,8 +181,6 @@ public enum MobData {
WHITE_TRADER_LLAMA("white", MobCompat.TRADER_LLAMA, "llama:WHITE", true),
BROWN_TRADER_LLAMA("brown", MobCompat.TRADER_LLAMA, "llama:BROWN", true),
GRAY_TRADER_LLAMA("gray", MobCompat.TRADER_LLAMA, "llama:GRAY", true),
RANDOM_SHULKER("random", MobCompat.SHULKER, Data.COLORABLE, true),
COLORABLE_SHULKER("", Arrays.stream(DyeColor.values()).map(color -> color.name().toLowerCase(Locale.ENGLISH)).collect(Collectors.toList()), MobCompat.SHULKER, Data.COLORABLE, true),
RED_FOX("red", MobCompat.FOX, "fox:RED", true),
SNOW_FOX("snow", MobCompat.FOX, "fox:SNOW", true),
SIZE_PHANTOM("", Collections.singletonList("<1-100>"), MobCompat.PHANTOM, Data.SIZE, true),
@@ -208,12 +206,13 @@ public enum MobData {
JUNGLE_BOAT("jungle", Boat.class, MobCompat.BoatVariant.JUNGLE, true),
REDWOOD_BOAT("redwood", Boat.class, MobCompat.BoatVariant.REDWOOD, true),
MANGROVE_BOAT("mangrove", Boat.class, MobCompat.BoatVariant.MANGROVE, true),
POPLAR_BOAT("poplar", Boat.class, MobCompat.BoatVariant.POPLAR, true),
OAK_BOAT("oak", Boat.class, MobCompat.BoatVariant.OAK, true),
SPRUCE_BOAT("spruce", Boat.class, MobCompat.BoatVariant.SPRUCE, true),
SADDLE_CAMEL("saddle", MobCompat.CAMEL, Data.CAMELSADDLE, true),
PALE_WOLF("pale", EntityType.WOLF, "wolf:PALE", true),
SPOTTED_WOLF("spotted", EntityType.WOLF, "wolf:PALE", true),
SNOWY_WOLF("snowy", EntityType.WOLF, "wolf:PALE", true),
SPOTTED_WOLF("spotted", EntityType.WOLF, "wolf:SPOTTED", true),
SNOWY_WOLF("snowy", EntityType.WOLF, "wolf:SNOWY", true),
BLACK_WOLF("black", EntityType.WOLF, "wolf:BLACK", true),
ASHEN_WOLF("ashen", EntityType.WOLF, "wolf:ASHEN", true),
RUSTY_WOLF("rusty", EntityType.WOLF, "wolf:RUSTY", true),
@@ -156,7 +156,7 @@ public class RandomTeleport implements IConf {
// Prompts caching random valid locations, up to a maximum number of attempts.
public void cacheRandomLocations(final String name) {
ess.runTaskAsynchronously(() -> {
ess.getServer().getScheduler().scheduleSyncDelayedTask(ess, () -> {
for (int i = 0; i < this.getFindAttempts(); ++i) {
calculateRandomLocation(getCenter(name), getMinRange(name), getMaxRange(name)).thenAccept(location -> {
if (isValidRandomLocation(location)) {
@@ -186,6 +186,7 @@ public class RandomTeleport implements IConf {
// Calculates a random location asynchronously.
private CompletableFuture<Location> calculateRandomLocation(final Location center, final double minRange, final double maxRange) {
final CompletableFuture<Location> future = new CompletableFuture<>();
// Find an equally distributed offset by randomly rotating a point inside a rectangle about the origin
final double rectX = RANDOM.nextDouble() * (maxRange - minRange) + minRange;
final double rectZ = RANDOM.nextDouble() * (maxRange + minRange) - minRange;
@@ -213,14 +214,15 @@ public class RandomTeleport implements IConf {
360 * RANDOM.nextFloat() - 180,
0
);
return PaperLib.getChunkAtAsync(location).thenApply(chunk -> { // FIXME: use thenApplyAsync + an Executor that runs tasks on the correct region thread to be fully safe.
PaperLib.getChunkAtAsync(location).thenAccept(chunk -> {
if (World.Environment.NETHER.equals(center.getWorld().getEnvironment())) {
location.setY(getNetherYAt(location));
} else {
location.setY(center.getWorld().getHighestBlockYAt(location) + HIGHEST_BLOCK_Y_OFFSET);
}
return location;
future.complete(location);
});
return future;
}
// Returns an appropriate elevation for a given location in the nether, or MIN_VALUE if none is found
@@ -147,6 +147,7 @@ public class Settings implements net.ess3.api.ISettings {
private boolean logCommandBlockCommands;
private boolean logConsoleCommands;
private Set<Predicate<String>> nickBlacklist;
private boolean resetNickOnNameChange;
private double maxProjectileSpeed;
private boolean removeEffectsOnHeal;
private Map<String, String> worldAliases;
@@ -296,6 +297,11 @@ public class Settings implements net.ess3.api.ISettings {
return config.getBoolean("force-safe-teleport-location", false);
}
@Override
public boolean isConsiderWorldHeightForTeleportSafety() {
return config.getBoolean("consider-world-height-for-teleport-safety", false);
}
@Override
public boolean isTeleportPassengerDismount() {
return config.getBoolean("teleport-passenger-dismount", true);
@@ -508,11 +514,20 @@ public class Settings implements net.ess3.api.ISettings {
return config.getString("nickname-prefix", "~");
}
private boolean _resetNickOnNameChange() {
return config.getBoolean("reset-nick-on-name-change", false);
}
@Override
public String getNicknamePrefix() {
return nicknamePrefix;
}
@Override
public boolean isResetNickOnNameChange() {
return resetNickOnNameChange;
}
@Override
public double getTeleportCooldown() {
return config.getDouble("teleport-cooldown", 0);
@@ -837,7 +852,7 @@ public class Settings implements net.ess3.api.ISettings {
if (reloadCount.get() < 2) {
// on startup: add plugins again in case they registered commands with the new API
// we need to schedule this task before any of the below tasks using _addAlternativeCommand.
ess.scheduleGlobalDelayedTask(() -> {
ess.scheduleSyncDelayedTask(() -> {
for (final Plugin plugin : ess.getServer().getPluginManager().getPlugins()) {
if (plugin.isEnabled()) {
ess.getAlternativeCommandsHandler().addPlugin(plugin);
@@ -863,7 +878,7 @@ public class Settings implements net.ess3.api.ISettings {
// This is 2 because Settings are reloaded twice in the startup lifecycle
if (reloadCount.get() < 2) {
ess.scheduleGlobalDelayedTask(() -> _addAlternativeCommand(effectiveAlias, toDisable));
ess.scheduleSyncDelayedTask(() -> _addAlternativeCommand(effectiveAlias, toDisable));
} else {
_addAlternativeCommand(effectiveAlias, toDisable);
}
@@ -878,7 +893,7 @@ public class Settings implements net.ess3.api.ISettings {
ess.getLogger().log(Level.INFO, "Syncing commands");
}
if (reloadCount.get() < 2) {
ess.scheduleGlobalDelayedTask(syncCommandsProvider::syncCommands);
ess.scheduleSyncDelayedTask(syncCommandsProvider::syncCommands);
} else {
syncCommandsProvider.syncCommands();
}
@@ -886,6 +901,7 @@ public class Settings implements net.ess3.api.ISettings {
}
nicknamePrefix = _getNicknamePrefix();
resetNickOnNameChange = _resetNickOnNameChange();
operatorColor = _getOperatorColor();
changePlayerListName = _changePlayerListName();
configDebug = _isDebug();
@@ -1320,6 +1336,11 @@ public class Settings implements net.ess3.api.ISettings {
return config.getBoolean("cancel-afk-on-chat", true);
}
@Override
public boolean cancelAfkOnFish() {
return config.getBoolean("cancel-afk-on-fish", true);
}
@Override
public boolean sleepIgnoresAfkPlayers() {
return sleepIgnoresAfkPlayers;
@@ -32,6 +32,10 @@ public final class SpawnMob {
private static final Material GOLDEN_HELMET = EnumUtil.getMaterial("GOLDEN_HELMET", "GOLD_HELMET");
private static final Material GOLDEN_CHESTPLATE = EnumUtil.getMaterial("GOLDEN_CHESTPLATE", "GOLD_CHESTPLATE");
private static final Material COPPER_HELMET = EnumUtil.getMaterial("COPPER_HELMET");
private static final Material COPPER_CHESTPLATE = EnumUtil.getMaterial("COPPER_CHESTPLATE");
private static final Material COPPER_LEGGINGS = EnumUtil.getMaterial("COPPER_LEGGINGS");
private static final Material COPPER_BOOTS = EnumUtil.getMaterial("COPPER_BOOTS");
private static final Material GOLDEN_LEGGINGS = EnumUtil.getMaterial("GOLDEN_LEGGINGS", "GOLD_LEGGINGS");
private static final Material GOLDEN_BOOTS = EnumUtil.getMaterial("GOLDEN_BOOTS", "GOLD_BOOTS");
private static final Material GOLDEN_SWORD = EnumUtil.getMaterial("GOLDEN_SWORD", "GOLD_SWORD");
@@ -210,6 +214,11 @@ public final class SpawnMob {
invent.setLeggings(new ItemStack(Material.NETHERITE_LEGGINGS, 1));
invent.setChestplate(new ItemStack(Material.NETHERITE_CHESTPLATE, 1));
invent.setHelmet(new ItemStack(Material.NETHERITE_HELMET, 1));
} else if (inputData.contains("copper") && COPPER_HELMET != null) {
invent.setBoots(new ItemStack(COPPER_BOOTS, 1));
invent.setLeggings(new ItemStack(COPPER_LEGGINGS, 1));
invent.setChestplate(new ItemStack(COPPER_CHESTPLATE, 1));
invent.setHelmet(new ItemStack(COPPER_HELMET, 1));
} else if (inputData.contains("diamond")) {
invent.setBoots(new ItemStack(Material.DIAMOND_BOOTS, 1));
invent.setLeggings(new ItemStack(Material.DIAMOND_LEGGINGS, 1));
@@ -23,6 +23,7 @@ import net.ess3.api.events.JailStatusChangeEvent;
import net.ess3.api.events.MuteStatusChangeEvent;
import net.ess3.api.events.UserBalanceUpdateEvent;
import net.ess3.provider.PlayerLocaleProvider;
import net.essentialsx.api.v2.events.PreTransactionEvent;
import net.essentialsx.api.v2.events.TransactionEvent;
import net.essentialsx.api.v2.services.mail.MailSender;
import org.bukkit.Location;
@@ -146,6 +147,19 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
return result;
}
@Override
public boolean isAuthorizedCached(final String node) {
if (Essentials.TESTING) {
return false;
}
final boolean result = isAuthorizedCachedCheck(node);
if (ess.getSettings().isDebug()) {
ess.getLogger().log(Level.INFO, "checking if " + base.getName() + " has " + node + " (cached) - " + result);
}
return result;
}
@Override
public boolean isPermissionSet(final String node) {
if (Essentials.TESTING) {
@@ -185,6 +199,24 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
}
}
private boolean isAuthorizedCachedCheck(final String node) {
if (base instanceof OfflinePlayerStub) {
return false;
}
try {
return ess.getPermissionsHandler().hasPermissionCached(base, node);
} catch (final Exception ex) {
if (ess.getSettings().isDebug()) {
ess.getLogger().log(Level.SEVERE, "Permission System Error: " + ess.getPermissionsHandler().getName() + " returned: " + ex.getMessage(), ex);
} else {
ess.getLogger().log(Level.SEVERE, "Permission System Error: " + ess.getPermissionsHandler().getName() + " returned: " + ex.getMessage());
}
return false;
}
}
private boolean isPermSetCheck(final String node) {
if (base instanceof OfflinePlayerStub) {
return false;
@@ -263,12 +295,20 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
payUser(reciever, value, UserBalanceUpdateEvent.Cause.UNKNOWN);
}
public void payUser(final User reciever, final BigDecimal value, final UserBalanceUpdateEvent.Cause cause) throws Exception {
public void payUser(final User reciever, BigDecimal value, final UserBalanceUpdateEvent.Cause cause) throws Exception {
if (value.compareTo(BigDecimal.ZERO) < 1) {
throw new Exception(tlLocale(playerLocale, "payMustBePositive"));
}
if (canAfford(value)) {
// Call an event for pre-transaction
final PreTransactionEvent preTransactionEvent = new PreTransactionEvent(this.getSource(), reciever, value);
ess.getServer().getPluginManager().callEvent(preTransactionEvent);
if (preTransactionEvent.isCancelled()) {
return;
}
value = preTransactionEvent.getAmount();
setMoney(getMoney().subtract(value), cause);
reciever.setMoney(reciever.getMoney().add(value), cause);
sendTl("moneySentTo", AdventureUtil.parsed(NumberUtil.displayCurrency(value, ess)), reciever.getDisplayName());
@@ -577,7 +617,8 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
final BigDecimal value = _getMoney();
final long elapsed = System.nanoTime() - start;
if (elapsed > ess.getSettings().getEconomyLagWarning()) {
ess.getLogger().log(Level.INFO, "Lag Notice - Slow Economy Response - Request took over {0}ms!", elapsed / 1000000.0);
ess.getLogger().log(Level.INFO, String.format(Locale.ROOT,
"Lag Notice - Slow Economy Response - Request took over %.3fms!", elapsed / 1_000_000.0));
}
return value;
}
@@ -782,6 +823,24 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
return false;
}
// Notifies this (muted) user that they have been silenced, including the reason and remaining time if applicable.
public void notifyMuted() {
final String dateDiff = getMuteTimeout() > 0 ? DateUtil.formatDateDiff(getMuteTimeout()) : null;
if (dateDiff == null) {
if (hasMuteReason()) {
sendTl("voiceSilencedReason", getMuteReason());
} else {
sendTl("voiceSilenced");
}
} else {
if (hasMuteReason()) {
sendTl("voiceSilencedReasonTime", dateDiff, getMuteReason());
} else {
sendTl("voiceSilencedTime", dateDiff);
}
}
}
@Override
public long getLastActivityTime() {
return this.lastActivity;
@@ -821,7 +880,7 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
public void updateActivityOnChat(final boolean broadcast) {
if (ess.getSettings().cancelAfkOnChat()) {
//Chat happens async, make sure we have a sync context
ess.scheduleEntityDelayedTask(base, () -> updateActivity(broadcast, AfkStatusChangeEvent.Cause.CHAT));
ess.scheduleSyncDelayedTask(() -> updateActivity(broadcast, AfkStatusChangeEvent.Cause.CHAT));
}
}
@@ -847,7 +906,7 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
for (final User user : ess.getOnlineUsers()) {
if (user.isAuthorized("essentials.kick.notify")) {
user.sendTl("playerKicked", Console.DISPLAY_NAME, getName(), user.playerTl("autoAfkKickReason", kickTime));
user.sendTl("playerKicked", Console.displayName(), getName(), user.playerTl("autoAfkKickReason", kickTime));
}
}
} else {
@@ -863,7 +922,7 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
}
}
final long autoafk = ess.getSettings().getAutoAfk();
if (!isAfk() && autoafk > 0 && lastActivity + autoafk * 1000 < System.currentTimeMillis() && isAuthorized("essentials.afk.auto")) {
if (!isAfk() && autoafk > 0 && lastActivity + autoafk * 1000 < System.currentTimeMillis() && isAuthorizedCached("essentials.afk.auto")) {
setAfk(true, AfkStatusChangeEvent.Cause.ACTIVITY);
if (isAfk() && !isHidden()) {
setDisplayNick();
@@ -42,7 +42,9 @@ public abstract class UserData extends PlayerExtension implements IConf {
super(base);
this.ess = (Essentials) ess;
final File folder = new File(ess.getDataFolder(), "userdata");
if (!folder.exists() && !folder.mkdirs()) {
// mkdirs() may return false if another thread created the folder concurrently,
// so fall back to checking whether the directory now exists to avoid a race.
if (!folder.isDirectory() && !folder.mkdirs() && !folder.isDirectory()) {
throw new RuntimeException("Unable to create userdata folder!");
}
@@ -469,6 +471,15 @@ public abstract class UserData extends PlayerExtension implements IConf {
config.save();
}
public boolean isFlyModeEnabled() {
return holder.flyMode();
}
public void setFlyModeEnabled(final boolean set) {
holder.flyMode(set);
config.save();
}
public boolean getMuted() {
return holder.muted();
}
@@ -21,17 +21,18 @@ import java.util.Locale;
public class SpigotAdventureFacet implements AdventureFacet {
private static final LegacyComponentSerializer LEGACY_SERIALIZER;
private static final LegacyComponentSerializer LEGACY_SERIALIZER_URLS;
private static final MiniMessage MINI_MESSAGE_NO_TAGS;
static {
final LegacyComponentSerializer.Builder builder = LegacyComponentSerializer.builder()
.flattener(ComponentFlattener.basic())
.extractUrls(AbstractChatEvent.URL_PATTERN)
.useUnusualXRepeatedCharacterHexFormat();
if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_16_1_R01)) {
builder.hexColors();
}
LEGACY_SERIALIZER = builder.build();
LEGACY_SERIALIZER_URLS = builder.extractUrls(AbstractChatEvent.URL_PATTERN).build();
MINI_MESSAGE_NO_TAGS = MiniMessage.builder().strict(true).build();
}
@@ -75,7 +76,7 @@ public class SpigotAdventureFacet implements AdventureFacet {
@Override
public String legacyToMini(String message) {
return legacyToMini(message, true);
return legacyToMini(message, false);
}
@Override
@@ -127,6 +128,11 @@ public class SpigotAdventureFacet implements AdventureFacet {
return miniMessageInstance.stripTags(input);
}
@Override
public String legacyToMiniWithUrls(String message) {
return miniMessageInstance.serialize(LEGACY_SERIALIZER_URLS.deserialize(message));
}
@Override
public String escapeTags(String input) {
return miniMessageInstance.escapeTags(input);
@@ -323,7 +323,13 @@ public class Economy {
throw new IllegalArgumentException("Economy user cannot be null");
}
final BigDecimal result = getMoneyExact(user).add(amount, MATH_CONTEXT);
setMoney(user, result);
// Deposits (positive amounts) always improve the balance, so minMoney and
// loan checks don't apply — they exist to prevent the balance from dropping.
if (amount.signum() <= 0) {
setMoney(user, result);
} else {
user.setMoney(result, UserBalanceUpdateEvent.Cause.API);
}
Trade.log("API", "Add", "API", user.getName(), new Trade(amount, ess), null, null, null, result, ess);
}
@@ -44,7 +44,7 @@ public class Commandbalancetop extends EssentialsCommand {
new TextPager(cache).showPage(Integer.toString(page), null, "balancetop", sender);
};
if (sender.getSender() instanceof BlockCommandSender) {
ess.scheduleGlobalDelayedTask(runnable);
ess.scheduleSyncDelayedTask(runnable);
} else {
runnable.run();
}
@@ -42,7 +42,7 @@ public class Commandban extends EssentialsCommand {
}
final String senderName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.DISPLAY_NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.displayName();
final String banReason;
if (args.length > 1) {
banReason = FormatUtil.replaceFormat(getFinalArg(args, 1).replace("\\n", "\n").replace("|", "\n"));
@@ -26,7 +26,7 @@ public class Commandbanip extends EssentialsCommand {
}
final String senderName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.DISPLAY_NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.displayName();
String ipAddress;
if (FormatUtil.validIP(args[0])) {
@@ -23,7 +23,7 @@ public class Commandbeezooka extends EssentialsCommand {
final Entity bee = Mob.BEE.spawn(user.getWorld(), server, user.getBase().getEyeLocation());
bee.setVelocity(user.getBase().getEyeLocation().getDirection().multiply(2));
ess.scheduleEntityDelayedTask(bee, () -> {
ess.scheduleSyncDelayedTask(() -> {
final Location loc = bee.getLocation();
bee.remove();
loc.getWorld().createExplosion(loc, 0F);
@@ -1,6 +1,7 @@
package com.earth2me.essentials.commands;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.adventure.AdventureUtil;
import com.earth2me.essentials.utils.FormatUtil;
import org.bukkit.Server;
@@ -15,6 +16,9 @@ public class Commandbroadcast extends EssentialsCommand {
throw new NotEnoughArgumentsException();
}
ess.broadcastTl("broadcast", FormatUtil.replaceFormat(getFinalArg(args, 0)).replace("\\n", "\n"), sender.getDisplayName());
final String message = FormatUtil.replaceFormat(getFinalArg(args, 0)).replace("\\n", "\n");
ess.broadcastTl("broadcast",
AdventureUtil.parsed(ess.getAdventureFacet().legacyToMiniWithUrls(ess.getAdventureFacet().escapeTags(message))),
sender.getDisplayName());
}
}
@@ -54,7 +54,11 @@ public class Commandbroadcastworld extends EssentialsCommand {
if (message.isEmpty()) {
throw new NotEnoughArgumentsException();
}
ess.broadcastTl(null, u -> !u.getBase().getWorld().equals(world), true, "broadcast", FormatUtil.replaceFormat(message).replace("\\n", "\n"), AdventureUtil.parsed(ess.getAdventureFacet().legacyToMini(name)));
final String formatted = FormatUtil.replaceFormat(message).replace("\\n", "\n");
ess.broadcastTl(null, u -> !u.getBase().getWorld().equals(world), true, "broadcast",
AdventureUtil.parsed(
ess.getAdventureFacet().legacyToMiniWithUrls(ess.getAdventureFacet().escapeTags(formatted))),
AdventureUtil.parsed(ess.getAdventureFacet().legacyToMini(name)));
}
@Override
@@ -19,8 +19,12 @@ public class Commandburn extends EssentialsCommand {
}
final User user = getPlayer(server, sender, args, 0);
user.getBase().setFireTicks(Integer.parseInt(args[1]) * 20);
sender.sendTl("burnMsg", user.getDisplayName(), Integer.parseInt(args[1]));
final int seconds = Integer.parseInt(args[1]);
final int fireTicks = (int) Math.max(0L, Math.min((long) seconds * 20, Integer.MAX_VALUE));
user.getBase().setFireTicks(fireTicks);
sender.sendTl("burnMsg", user.getDisplayName(), Math.max(0, seconds));
}
@Override
@@ -12,7 +12,8 @@ public class Commanddisposal extends EssentialsCommand {
@Override
protected void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception {
user.sendTl("openingDisposal");
user.getBase().openInventory(ess.getServer().createInventory(user.getBase(), 36, user.playerTl("disposal")));
final String title = ess.getAdventureFacet().miniToLegacy(user.playerTl("disposal"));
user.getBase().openInventory(ess.getServer().createInventory(user.getBase(), 36, title));
}
}
@@ -9,6 +9,7 @@ import com.google.common.collect.Lists;
import net.ess3.api.MaxMoneyException;
import net.ess3.api.TranslatableException;
import net.ess3.api.events.UserBalanceUpdateEvent;
import net.ess3.provider.PlayerLocaleProvider;
import org.bukkit.Server;
import java.math.BigDecimal;
@@ -34,7 +35,14 @@ public class Commandeco extends EssentialsLoopCommand {
try {
cmd = EcoCommands.valueOf(args[0].toUpperCase(Locale.ENGLISH));
isPercent = cmd != EcoCommands.RESET && args[2].endsWith("%");
amount = (cmd == EcoCommands.RESET) ? ess.getSettings().getStartingBalance() : new BigDecimal(args[2].replaceAll("[^0-9\\.]", ""));
if (cmd == EcoCommands.RESET) {
amount = ess.getSettings().getStartingBalance();
} else if (sender.isPlayer() && ess.getSettings().isPerPlayerLocale()) {
final String playerLocale = ess.provider(PlayerLocaleProvider.class).getLocale(sender.getPlayer());
amount = NumberUtil.parseStringToBDecimal(args[2], ess.getUser(sender.getPlayer()).getPlayerLocale(playerLocale));
} else {
amount = NumberUtil.parseStringToBDecimal(args[2]);
}
} catch (final Exception ex) {
throw new NotEnoughArgumentsException(ex);
}
@@ -8,6 +8,7 @@ import com.earth2me.essentials.commands.essentials.HomesCommand;
import com.earth2me.essentials.commands.essentials.ItemTestCommand;
import com.earth2me.essentials.commands.essentials.MooCommand;
import com.earth2me.essentials.commands.essentials.NyanCommand;
import com.earth2me.essentials.commands.essentials.ReloadCommand;
import com.earth2me.essentials.commands.essentials.UsermapCommand;
import com.earth2me.essentials.commands.essentials.VersionCommand;
@@ -19,6 +20,7 @@ public class Commandessentials extends EssentialsTreeCommand {
registerNode(new DebugCommand());
registerNode(new CommandMapCommand());
registerNode(new DumpCommand());
registerNode(new ReloadCommand());
registerNode(new CleanupCommand());
registerNode(new HomesCommand());
registerNode(new UsermapCommand());
@@ -147,15 +147,21 @@ public class Commandexp extends EssentialsLoopCommand {
}
return options;
} else if (args.length == 2) {
if ((args[0].equalsIgnoreCase("set") && user.isAuthorized("essentials.exp.set")) || (args[0].equalsIgnoreCase("give") && user.isAuthorized("essentials.exp.give")) || (args[0].equalsIgnoreCase("take") && user.isAuthorized("essentials.exp.take"))) {
final ExpCommands cmd;
try {
cmd = ExpCommands.valueOf(args[0].toUpperCase(Locale.ENGLISH));
} catch (final IllegalArgumentException e) {
return Collections.emptyList();
}
if (cmd.hasOtherPermission(user)) {
return getPlayers(user);
}
if (cmd.hasPermission(user) && cmd != ExpCommands.SHOW && cmd != ExpCommands.RESET) {
final String levellessArg = args[1].toLowerCase(Locale.ENGLISH).replaceAll("l", "");
if (NumberUtil.isInt(levellessArg)) {
return Lists.newArrayList(levellessArg + "l");
}
}
if (user.isAuthorized("essentials.exp.others")) {
return getPlayers(user);
}
} else if (args.length == 3 && !(args[0].equalsIgnoreCase("show") || args[0].equalsIgnoreCase("reset"))) {
final String levellessArg = args[2].toLowerCase(Locale.ENGLISH).replaceAll("l", "");
if (NumberUtil.isInt(levellessArg)) {
@@ -34,6 +34,8 @@ public class Commandfly extends EssentialsToggleCommand {
user.getBase().setFallDistance(0f);
user.getBase().setAllowFlight(enabled);
user.setFlyModeEnabled(enabled);
if (!user.getBase().getAllowFlight()) {
user.getBase().setFlying(false);
}
@@ -3,13 +3,15 @@ package com.earth2me.essentials.commands;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.NumberUtil;
import net.ess3.provider.WorldTileEntityCountProvider;
import net.ess3.provider.TileEntityProvider;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.Server;
import org.bukkit.World;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.logging.Level;
public class Commandgc extends EssentialsCommand {
public Commandgc() {
@@ -18,7 +20,7 @@ public class Commandgc extends EssentialsCommand {
@Override
protected void run(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception {
final double tps = 20d; //TODO
final double tps = ess.getTimer().getAverageTPS();
final ChatColor color;
if (tps >= 18.0) {
color = ChatColor.GREEN;
@@ -35,7 +37,7 @@ public class Commandgc extends EssentialsCommand {
sender.sendTl("gcfree", Runtime.getRuntime().freeMemory() / 1024 / 1024);
final List<World> worlds = server.getWorlds();
final WorldTileEntityCountProvider worldTileEntityCountProvider = ess.provider(WorldTileEntityCountProvider.class);
final TileEntityProvider tileEntityProvider = ess.provider(TileEntityProvider.class);
for (final World w : worlds) {
String worldType = "World";
switch (w.getEnvironment()) {
@@ -47,7 +49,15 @@ public class Commandgc extends EssentialsCommand {
break;
}
final int tileEntities = worldTileEntityCountProvider.getTileEntityCount(w);
int tileEntities = 0;
try {
for (final Chunk chunk : w.getLoadedChunks()) {
tileEntities += tileEntityProvider.getTileEntities(chunk).length;
}
} catch (final java.lang.ClassCastException ex) {
ess.getLogger().log(Level.SEVERE, "Corrupted chunk data on world " + w, ex);
}
sender.sendTl("gcWorld", worldType, w.getName(), w.getLoadedChunks().length, w.getEntities().size(), tileEntities);
}
@@ -54,7 +54,7 @@ public class Commandhelp extends EssentialsCommand {
user.sendTl("commandHelpLine3");
if (essCommand != null && !essCommand.getUsageStrings().isEmpty()) {
for (Map.Entry<String, String> usage : essCommand.getUsageStrings().entrySet()) {
user.sendTl("commandHelpLineUsage", AdventureUtil.parsed(usage.getKey().replace("<command>", cmd)), AdventureUtil.parsed(usage.getValue()));
user.sendTl("commandHelpLineUsage", AdventureUtil.parsed(usage.getKey().replace("<command>", cmd)), AdventureUtil.parsed(user.playerTl(usage.getValue())));
}
} else {
user.sendMessage(bukkit.getUsage());
@@ -41,7 +41,10 @@ public class Commandhome extends EssentialsCommand {
}
}
try {
if ("bed".equalsIgnoreCase(homeName) && user.isAuthorized("essentials.home.bed")) {
if ("bed".equalsIgnoreCase(homeName)) {
if (!user.isAuthorized("essentials.home.bed")) {
throw new TranslatableException("noAccessCommand");
}
if (!player.getBase().isOnline() || player.getBase() instanceof OfflinePlayerStub) {
throw new TranslatableException("bedOffline");
}
@@ -73,10 +76,15 @@ public class Commandhome extends EssentialsCommand {
final List<String> homes = finalPlayer.getHomes();
if (homes.isEmpty() && finalPlayer.equals(user)) {
if (ess.getSettings().isSpawnIfNoHome()) {
final UserTeleportHomeEvent event = new UserTeleportHomeEvent(user, null, bed != null ? bed : finalPlayer.getWorld().getSpawnLocation(), bed != null ? UserTeleportHomeEvent.HomeType.BED : UserTeleportHomeEvent.HomeType.SPAWN);
final boolean useBed = bed != null && user.isAuthorized("essentials.home.bed");
final UserTeleportHomeEvent event = new UserTeleportHomeEvent(user, null, useBed ? bed : finalPlayer.getWorld().getSpawnLocation(), useBed ? UserTeleportHomeEvent.HomeType.BED : UserTeleportHomeEvent.HomeType.SPAWN);
server.getPluginManager().callEvent(event);
if (!event.isCancelled()) {
user.getAsyncTeleport().respawn(charge, TeleportCause.COMMAND, getNewExceptionFuture(user.getSource(), commandLabel));
if (useBed) {
user.getAsyncTeleport().respawn(charge, TeleportCause.COMMAND, getNewExceptionFuture(user.getSource(), commandLabel));
} else {
user.getAsyncTeleport().teleport(finalPlayer.getWorld().getSpawnLocation(), charge, TeleportCause.COMMAND, getNewExceptionFuture(user.getSource(), commandLabel));
}
}
} else {
showError(user.getBase(), new TranslatableException("noHomeSetPlayer"), commandLabel);
@@ -0,0 +1,23 @@
package com.earth2me.essentials.commands;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.utils.StringUtil;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import java.util.stream.Collectors;
public class Commandjailedplayers extends EssentialsCommand {
public Commandjailedplayers() {
super("jailedplayers");
}
@Override
protected void run(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception {
if (ess.getJailedPlayers() != null && !ess.getJailedPlayers().isEmpty()) {
sender.sendTl("jailedPlayersList", StringUtil.joinList(", ", ess.getJailedPlayers().stream().map(Player::getName).collect(Collectors.toList())));
} else {
sender.sendTl("jailedPlayersEmpty");
}
}
}
@@ -50,7 +50,7 @@ public class Commandkick extends EssentialsCommand {
kickReason = event.getReason();
target.getBase().kickPlayer(kickReason);
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.DISPLAY_NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.displayName();
final String tlKey = "playerKicked";
final Object[] objects = {senderDisplayName, target.getName(), kickReason};
@@ -32,15 +32,13 @@ public class Commandkill extends EssentialsLoopCommand {
}
final DamageEventProvider provider = ess.provider(DamageEventProvider.class);
ess.scheduleEntityDelayedTask(matchPlayer, () -> {
final EntityDamageEvent ede = provider.callDamageEvent(matchPlayer, sender.isPlayer() && sender.getPlayer().getName().equals(matchPlayer.getName()) ? EntityDamageEvent.DamageCause.SUICIDE : EntityDamageEvent.DamageCause.CUSTOM, Float.MAX_VALUE);
if (ede.isCancelled() && sender.isPlayer() && !ess.getUser(sender.getPlayer()).isAuthorized("essentials.kill.force")) {
return;
}
ede.getEntity().setLastDamageCause(ede);
matchPlayer.setHealth(0);
sender.sendTl("kill", matchPlayer.getDisplayName());
});
final EntityDamageEvent ede = provider.callDamageEvent(matchPlayer, sender.isPlayer() && sender.getPlayer().getName().equals(matchPlayer.getName()) ? EntityDamageEvent.DamageCause.SUICIDE : EntityDamageEvent.DamageCause.CUSTOM, Float.MAX_VALUE);
if (ede.isCancelled() && sender.isPlayer() && !ess.getUser(sender.getPlayer()).isAuthorized("essentials.kill.force")) {
return;
}
ede.getEntity().setLastDamageCause(ede);
matchPlayer.setHealth(0);
sender.sendTl("kill", matchPlayer.getDisplayName());
}
@Override
@@ -49,7 +49,7 @@ public class Commandkittycannon extends EssentialsCommand {
@Override
protected void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception {
final Entity ocelot = Mob.CAT.getType() == null ? spawnOcelot(server, user) : spawnCat(server, user);
ess.scheduleEntityDelayedTask(ocelot, () -> {
ess.scheduleSyncDelayedTask(() -> {
final Location loc = ocelot.getLocation();
ocelot.remove();
loc.getWorld().createExplosion(loc, 0F);
@@ -33,17 +33,15 @@ public class Commandlightning extends EssentialsLoopCommand {
}
final int finalPower = power;
loopOnlinePlayersConsumer(server, sender, false, true, args[0], player -> {
ess.scheduleEntityDelayedTask(player.getBase(), () -> {
sender.sendTl("lightningUse", player.getDisplayName());
final LightningStrike strike = player.getBase().getWorld().strikeLightningEffect(player.getBase().getLocation());
sender.sendTl("lightningUse", player.getDisplayName());
final LightningStrike strike = player.getBase().getWorld().strikeLightningEffect(player.getBase().getLocation());
if (!player.isGodModeEnabled()) {
player.getBase().damage(finalPower, strike);
}
if (ess.getSettings().warnOnSmite()) {
player.sendTl("lightningSmited");
}
});
if (!player.isGodModeEnabled()) {
player.getBase().damage(finalPower, strike);
}
if (ess.getSettings().warnOnSmite()) {
player.sendTl("lightningSmited");
}
});
loopOnlinePlayers(server, sender, true, true, args[0], null);
}
@@ -36,11 +36,13 @@ public class Commandmore extends EssentialsCommand {
if (!NumberUtil.isPositiveInt(args[0])) {
throw new TranslatableException("nonZeroPosNumber");
}
newStackSize += Integer.parseInt(args[0]);
if (newStackSize > (canOversized ? ess.getSettings().getOversizedStackSize() : stack.getMaxStackSize())) {
user.sendTl(canOversized ? "fullStackDefaultOversize" : "fullStackDefault", canOversized ? ess.getSettings().getOversizedStackSize() : stack.getMaxStackSize());
newStackSize = canOversized ? ess.getSettings().getOversizedStackSize() : stack.getMaxStackSize();
final int cap = canOversized ? ess.getSettings().getOversizedStackSize() : stack.getMaxStackSize();
final long newSizeLong = (long) stack.getAmount() + Integer.parseInt(args[0]);
if (newSizeLong > cap) {
user.sendTl(canOversized ? "fullStackDefaultOversize" : "fullStackDefault", cap);
newStackSize = cap;
} else {
newStackSize = (int) newSizeLong;
}
} else if (canOversized) {
newStackSize = ess.getSettings().getOversizedStackSize();
@@ -4,9 +4,7 @@ import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.Console;
import com.earth2me.essentials.User;
import com.earth2me.essentials.messaging.IMessageRecipient;
import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.FormatUtil;
import net.ess3.api.TranslatableException;
import org.bukkit.Server;
import java.util.Collections;
@@ -27,20 +25,15 @@ public class Commandmsg extends EssentialsLoopCommand {
final boolean canWildcard = sender.isAuthorized("essentials.msg.multiple");
if (sender.isPlayer()) {
final User user = ess.getUser(sender.getPlayer());
if (user.isMuted()) {
final String dateDiff = user.getMuteTimeout() > 0 ? DateUtil.formatDateDiff(user.getMuteTimeout()) : null;
if (dateDiff == null) {
throw new TranslatableException(user.hasMuteReason() ? "voiceSilencedReason" : "voiceSilenced", user.getMuteReason());
}
throw new TranslatableException(user.hasMuteReason() ? "voiceSilencedReasonTime" : "voiceSilencedTime", dateDiff, user.getMuteReason());
}
// Muted players are handled in SimpleMessageRecipient#sendMessage so that the message can still
// be shown to social spies before being dropped instead of being delivered to the recipient.
message = FormatUtil.formatMessage(user, "essentials.msg", message);
} else {
message = FormatUtil.replaceFormat(message);
}
// Sending messages to console
if (args[0].equalsIgnoreCase(Console.NAME) || args[0].equalsIgnoreCase(Console.DISPLAY_NAME)) {
if (args[0].equalsIgnoreCase(Console.NAME) || args[0].equalsIgnoreCase(Console.displayName())) {
final IMessageRecipient messageSender = sender.isPlayer() ? ess.getUser(sender.getPlayer()) : Console.getInstance();
messageSender.sendMessage(Console.getInstance(), message);
return;
@@ -21,6 +21,7 @@ public class Commandmute extends EssentialsCommand {
@Override
public void run(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception {
final boolean isUnmute = commandLabel.toLowerCase(java.util.Locale.ENGLISH).contains("unmute");
boolean nomatch = false;
if (args.length < 1) {
throw new NotEnoughArgumentsException();
@@ -32,11 +33,17 @@ public class Commandmute extends EssentialsCommand {
nomatch = true;
user = ess.getUser(new OfflinePlayerStub(args[0], ess.getServer()));
}
if (isUnmute && !user.getMuted()) {
sender.sendTl("playerNotMuted", user.getDisplayName());
return;
}
if (!user.getBase().isOnline() && sender.isPlayer()) {
if (!sender.isAuthorized("essentials.mute.offline")) {
throw new TranslatableException("muteExemptOffline");
}
} else if (user.isAuthorized("essentials.mute.exempt")) {
} else if (!isUnmute && user.isAuthorized("essentials.mute.exempt")) {
throw new TranslatableException("muteExempt");
}
@@ -44,7 +51,7 @@ public class Commandmute extends EssentialsCommand {
final String time;
String muteReason = null;
if (args.length > 1) {
if (!isUnmute && args.length > 1) {
time = args[1];
try {
muteTimestamp = DateUtil.parseDateDiff(time, true);
@@ -59,7 +66,7 @@ public class Commandmute extends EssentialsCommand {
}
}
final boolean willMute = (args.length > 1) || !user.getMuted();
final boolean willMute = !isUnmute && ((args.length > 1) || !user.getMuted());
final User controller = sender.isPlayer() ? ess.getUser(sender.getPlayer()) : null;
final MuteStatusChangeEvent event = new MuteStatusChangeEvent(user, controller, willMute, muteTimestamp, muteReason);
ess.getServer().getPluginManager().callEvent(event);
@@ -121,6 +128,8 @@ public class Commandmute extends EssentialsCommand {
} else {
sender.sendTl("unmutedPlayer", user.getDisplayName());
user.sendTl("playerUnmuted");
ess.getLogger().log(Level.INFO, ess.getAdventureFacet().miniToLegacy(tlLiteral("unmuteNotify", sender.getSender().getName(), user.getName())));
ess.broadcastTl(null, "essentials.mute.notify", "unmuteNotify", new Object[]{sender.getSender().getName(), user.getName()});
}
}
}
@@ -35,20 +35,17 @@ public class Commandnuke extends EssentialsCommand {
if (user == null) {
continue;
}
ess.scheduleEntityDelayedTask(user.getBase(), () -> {
user.sendTl("nuke");
final Location loc = user.getLocation();
final World world = loc.getWorld();
if (world != null) {
for (int x = -10; x <= 10; x += 5) {
for (int z = -10; z <= 10; z += 5) {
final TNTPrimed entity = world.spawn(new Location(world, loc.getBlockX() + x, world.getHighestBlockYAt(loc) + 64, loc.getBlockZ() + z), TNTPrimed.class);
entity.setMetadata(NUKE_META_KEY, new FixedMetadataValue(ess, true));
}
user.sendTl("nuke");
final Location loc = user.getLocation();
final World world = loc.getWorld();
if (world != null) {
for (int x = -10; x <= 10; x += 5) {
for (int z = -10; z <= 10; z += 5) {
final TNTPrimed entity = world.spawn(new Location(world, loc.getBlockX() + x, world.getHighestBlockYAt(loc) + 64, loc.getBlockZ() + z), TNTPrimed.class);
entity.setMetadata(NUKE_META_KEY, new FixedMetadataValue(ess, true));
}
}
});
}
}
}
@@ -4,7 +4,6 @@ import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.Console;
import com.earth2me.essentials.User;
import com.earth2me.essentials.messaging.IMessageRecipient;
import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.FormatUtil;
import net.ess3.api.TranslatableException;
import org.bukkit.Server;
@@ -24,14 +23,8 @@ public class Commandr extends EssentialsCommand {
final IMessageRecipient messageSender;
if (sender.isPlayer()) {
final User user = ess.getUser(sender.getPlayer());
if (user.isMuted()) {
final String dateDiff = user.getMuteTimeout() > 0 ? DateUtil.formatDateDiff(user.getMuteTimeout()) : null;
if (dateDiff == null) {
throw new TranslatableException(user.hasMuteReason() ? "voiceSilencedReason" : "voiceSilenced", user.getMuteReason());
}
throw new TranslatableException(user.hasMuteReason() ? "voiceSilencedReasonTime" : "voiceSilencedTime", dateDiff, user.getMuteReason());
}
// Muted players are handled in SimpleMessageRecipient#sendMessage so that the message can still
// be shown to social spies before being dropped instead of being delivered to the recipient.
message = FormatUtil.formatMessage(user, "essentials.msg", message);
messageSender = user;
} else {
@@ -13,8 +13,10 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.FurnaceRecipe;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.RecipeChoice;
@@ -158,7 +160,13 @@ public class Commandrecipe extends EssentialsCommand {
final User user = ess.getUser(sender.getPlayer());
user.getBase().closeInventory();
user.setRecipeSee(true);
final InventoryView view = user.getBase().openWorkbench(null, true);
final InventoryView view = openWorkbench(user);
if (view == null) {
user.setRecipeSee(false);
return;
}
final Inventory topInventory = ess.provider(InventoryViewProvider.class).getTopInventory(view);
final String[] recipeShape = recipe.getShape();
final Map<Character, ItemStack> ingredientMap = recipe.getIngredientMap();
for (int j = 0; j < recipeShape.length; j++) {
@@ -170,7 +178,7 @@ public class Commandrecipe extends EssentialsCommand {
if (VersionUtil.PRE_FLATTENING && item.getDurability() == Short.MAX_VALUE) {
item.setDurability((short) 0);
}
ess.provider(InventoryViewProvider.class).getTopInventory(view).setItem(j * 3 + k + 1, item);
topInventory.setItem(j * 3 + k + 1, item);
}
}
} else {
@@ -217,7 +225,11 @@ public class Commandrecipe extends EssentialsCommand {
final User user = ess.getUser(sender.getPlayer());
user.getBase().closeInventory();
user.setRecipeSee(true);
final InventoryView view = user.getBase().openWorkbench(null, true);
final InventoryView view = openWorkbench(user);
if (view == null) {
user.setRecipeSee(false);
return;
}
for (int i = 0; i < ingredients.size(); i++) {
final ItemStack item = ingredients.get(i);
if (VersionUtil.PRE_FLATTENING && item.getDurability() == Short.MAX_VALUE) {
@@ -239,6 +251,17 @@ public class Commandrecipe extends EssentialsCommand {
}
}
private InventoryView openWorkbench(final User user) {
final InventoryView view = user.getBase().openWorkbench(null, true);
// If InventoryOpenEvent is canceled, the items can end up in the player's own crafting grid
// which allows players to extract counterfeit items.
if (view == null)
return null;
final Inventory inventory = ess.provider(InventoryViewProvider.class).getTopInventory(view);
return inventory.getType() == InventoryType.WORKBENCH ? view : null;
}
public String getMaterialName(final CommandSource sender, final ItemStack stack) {
if (stack == null) {
return sender.tl("recipeNothing");
@@ -61,7 +61,7 @@ public class Commandseen extends EssentialsCommand {
return;
}
}
ess.runTaskAsynchronously(new Runnable() {
ess.getScheduler().runTaskAsynchronously(ess, new Runnable() {
@Override
public void run() {
final User userFromBukkit = ess.getUsers().getUser(args[0]);
@@ -25,6 +25,10 @@ public class Commandsetwarp extends EssentialsCommand {
throw new TranslatableException("invalidWarpName");
}
if (StringUtil.isReservedFileName(args[0])) {
throw new TranslatableException("invalidWarpName");
}
final IWarps warps = ess.getWarps();
Location warpLoc = null;
@@ -159,7 +159,7 @@ public class Commandskull extends EssentialsCommand {
final String shortNameFinal = shortOwnerName;
ess.scheduleEntityDelayedTask(user.getBase(), () -> {
ess.scheduleSyncDelayedTask(() -> {
stack.setItemMeta(skullMeta);
if (spawn) {
Inventories.addItem(receive.getBase(), stack);
@@ -37,7 +37,8 @@ public class Commandspeed extends EssentialsCommand {
final float speed;
final boolean isBypass = user.isAuthorized("essentials.speed.bypass");
if (args.length == 1) {
isFly = flyPermCheck(user, user.getBase().isFlying());
final boolean inferredFly = isFlyAlias(commandLabel) ? true : isWalkAlias(commandLabel) ? false : user.getBase().isFlying();
isFly = flyPermCheck(user, inferredFly);
speed = getMoveSpeed(args[0]);
} else {
isFly = flyPermCheck(user, isFlyMode(args[0]));
@@ -91,6 +92,14 @@ public class Commandspeed extends EssentialsCommand {
} else return !canWalk;
}
private boolean isFlyAlias(final String label) {
return label.contains("fly") || label.equalsIgnoreCase("fspeed") || label.equalsIgnoreCase("efspeed");
}
private boolean isWalkAlias(final String label) {
return label.contains("walk") || label.equalsIgnoreCase("wspeed") || label.equalsIgnoreCase("ewspeed");
}
private boolean isFlyMode(final String modeString) throws NotEnoughArgumentsException {
final boolean isFlyMode;
if (modeString.contains("fly") || modeString.equalsIgnoreCase("f")) {
@@ -53,7 +53,7 @@ public class Commandsudo extends EssentialsLoopCommand {
}
}
ess.scheduleEntityDelayedTask(user.getBase(), new SudoCommandTask());
ess.scheduleSyncDelayedTask(new SudoCommandTask());
}
}
}
@@ -49,7 +49,7 @@ public class Commandtempban extends EssentialsCommand {
}
final String senderName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.DISPLAY_NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.displayName();
ess.getServer().getBanList(BanList.Type.NAME).addBan(user.getName(), banReason, new Date(banTimestamp), senderName);
final String expiry = DateUtil.formatDateDiff(banTimestamp);
@@ -28,7 +28,7 @@ public class Commandtempbanip extends EssentialsCommand {
}
final String senderName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.DISPLAY_NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.displayName();
String ipAddress;
if (FormatUtil.validIP(args[0])) {
@@ -1,6 +1,7 @@
package com.earth2me.essentials.commands;
import com.earth2me.essentials.User;
import com.earth2me.essentials.utils.CommonPlaceholders;
import com.google.common.collect.Lists;
import org.bukkit.Server;
import org.bukkit.World;
@@ -21,17 +22,16 @@ public class Commandthunder extends EssentialsCommand {
final World world = user.getWorld();
final boolean setThunder = args[0].equalsIgnoreCase("true");
if (args.length == 1) {
ess.scheduleGlobalDelayedTask(() -> world.setThundering(setThunder));
user.sendTl("thunder", setThunder ? user.playerTl("enabled") : user.playerTl("disabled"));
world.setThundering(setThunder);
user.sendTl("thunder", CommonPlaceholders.enableDisable(user.getSource(), setThunder));
return;
}
ess.scheduleGlobalDelayedTask(() -> {
world.setThundering(setThunder);
world.setThunderDuration(Integer.parseInt(args[1]) * 20);
});
user.sendTl("thunderDuration", setThunder ? user.playerTl("enabled") : user.playerTl("disabled"), Integer.parseInt(args[1]));
world.setThundering(setThunder);
world.setThunderDuration(Integer.parseInt(args[1]) * 20);
user.sendTl("thunderDuration", CommonPlaceholders.enableDisable(user.getSource(), setThunder), Integer.parseInt(args[1]));
}
@Override
@@ -8,14 +8,17 @@ import com.google.common.collect.Lists;
import net.ess3.api.TranslatableException;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TreeSet;
@@ -81,16 +84,28 @@ public class Commandtime extends EssentialsCommand {
}
final StringJoiner joiner = new StringJoiner(", ");
final boolean timeAdd = add;
for (final World world : worlds) {
joiner.add(world.getName());
ess.scheduleGlobalDelayedTask(() -> {
long time = world.getTime();
if (!timeAdd) {
time -= time % 24000;
// Capture intended visible time for players with relative ptime before world time changes
final Map<Player, Long> ptimePlayers = new HashMap<>();
for (final Player player : world.getPlayers()) {
if (player.getPlayerTimeOffset() != 0 && player.isPlayerTimeRelative()) {
ptimePlayers.put(player, player.getPlayerTime());
}
world.setTime(time + (timeAdd ? 0 : 24000) + timeTick);
});
}
long time = world.getTime();
if (!add) {
time -= time % 24000;
}
world.setTime(time + (add ? 0 : 24000) + timeTick);
// Re-apply ptime offsets so players maintain their intended visible time
final long newWorldTime = world.getTime();
for (final Map.Entry<Player, Long> entry : ptimePlayers.entrySet()) {
entry.getKey().setPlayerTime(entry.getValue() - newWorldTime, true);
}
joiner.add(world.getName());
}
sender.sendTl(add ? "timeWorldAdd" : "timeWorldSet", DescParseTickFormat.formatTicks(timeTick), joiner.toString());
@@ -56,7 +56,7 @@ public class Commandtp extends EssentialsCommand {
throw new NotEnoughArgumentsException(user.playerTl("teleportInvalidLocation"));
}
final Location locpos = new Location(user.getWorld(), x2, y2, z2, user.getLocation().getYaw(), user.getLocation().getPitch());
user.getAsyncTeleport().now(locpos, false, TeleportCause.COMMAND, future);
user.getAsyncTeleport().now(locpos, true, TeleportCause.COMMAND, future);
future.thenAccept(success -> {
if (success) {
user.sendTl("teleporting", locpos.getWorld().getName(), locpos.getBlockX(), locpos.getBlockY(), locpos.getBlockZ());
@@ -120,7 +120,7 @@ public class Commandtp extends EssentialsCommand {
final User target = getPlayer(server, args, 0, true, false);
if (args.length == 2) {
final User toPlayer = getPlayer(server, args, 1, true, false);
target.sendTl("teleportAtoB", Console.DISPLAY_NAME, toPlayer.getDisplayName());
target.sendTl("teleportAtoB", Console.displayName(), toPlayer.getDisplayName());
target.getAsyncTeleport().now(toPlayer.getBase(), false, TeleportCause.COMMAND, getNewExceptionFuture(sender, commandLabel));
} else if (args.length > 3) {
final double x = args[1].startsWith("~") ? target.getLocation().getX() + (args[1].length() > 1 ? Double.parseDouble(args[1].substring(1)) : 0) : Double.parseDouble(args[1]);
@@ -21,7 +21,7 @@ public class Commandtpoffline extends EssentialsCommand {
final Location logout = target.getLogoutLocation();
if (logout == null) {
user.sendTl("teleportOfflineUnknown", user.getDisplayName());
user.sendTl("teleportOfflineUnknown", target.getDisplayName());
throw new NoChargeException();
}
@@ -38,7 +38,7 @@ public class Commandunban extends EssentialsCommand {
ess.getServer().getBanList(BanList.Type.NAME).pardon(name);
}
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.DISPLAY_NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.displayName();
ess.getLogger().log(Level.INFO, ess.getAdventureFacet().miniToLegacy(tlLiteral("playerUnbanned", senderDisplayName, name)));
ess.broadcastTl((IUser) null, "essentials.ban.notify", "playerUnbanned", senderDisplayName, name);
@@ -40,7 +40,7 @@ public class Commandunbanip extends EssentialsCommand {
}
ess.getServer().getBanList(BanList.Type.IP).pardon(ipAddress);
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.DISPLAY_NAME;
final String senderDisplayName = sender.isPlayer() ? sender.getPlayer().getDisplayName() : Console.displayName();
ess.getLogger().log(Level.INFO, ess.getAdventureFacet().miniToLegacy(tlLiteral("playerUnbanIpAddress", senderDisplayName, ipAddress)));
ess.broadcastTl((IUser) null, "essentials.banip.notify", "playerUnbanIpAddress", senderDisplayName, ipAddress);
@@ -30,18 +30,16 @@ public class Commandweather extends EssentialsCommand {
isStorm = args[0].equalsIgnoreCase("storm");
}
ess.scheduleGlobalDelayedTask(() -> {
final World world = user.getWorld();
final World world = user.getWorld();
if (args.length > 1) {
world.setStorm(isStorm);
world.setWeatherDuration(Integer.parseInt(args[1]) * 20);
user.sendTl(isStorm ? "weatherStormFor" : "weatherSunFor", world.getName(), args[1]);
return;
}
if (args.length > 1) {
world.setStorm(isStorm);
user.sendTl(isStorm ? "weatherStorm" : "weatherSun", world.getName());
});
world.setWeatherDuration(Integer.parseInt(args[1]) * 20);
user.sendTl(isStorm ? "weatherStormFor" : "weatherSunFor", world.getName(), args[1]);
return;
}
world.setStorm(isStorm);
user.sendTl(isStorm ? "weatherStorm" : "weatherSun", world.getName());
}
@Override
@@ -56,16 +54,14 @@ public class Commandweather extends EssentialsCommand {
throw new TranslatableException("weatherInvalidWorld", args[0]);
}
ess.scheduleLocationDelayedTask(world.getSpawnLocation(), () -> {
if (args.length > 2) {
world.setStorm(isStorm);
world.setWeatherDuration(Integer.parseInt(args[2]) * 20);
sender.sendTl(isStorm ? "weatherStormFor" : "weatherSunFor", world.getName(), args[2]);
return;
}
if (args.length > 2) {
world.setStorm(isStorm);
sender.sendTl(isStorm ? "weatherStorm" : "weatherSun", world.getName());
});
world.setWeatherDuration(Integer.parseInt(args[2]) * 20);
sender.sendTl(isStorm ? "weatherStormFor" : "weatherSunFor", world.getName(), args[2]);
return;
}
world.setStorm(isStorm);
sender.sendTl(isStorm ? "weatherStorm" : "weatherSun", world.getName());
}
@Override
@@ -56,7 +56,7 @@ public abstract class EssentialsCommand implements IEssentialsCommand {
//noinspection InfiniteLoopStatement
while (true) {
final String baseKey = name + "CommandUsage" + i;
addUsageString(tlLiteral(baseKey), tlLiteral(baseKey + "Description"));
addUsageString(tlLiteral(baseKey), baseKey + "Description");
i++;
}
} catch (MissingResourceException ignored) {
@@ -1,7 +1,9 @@
package com.earth2me.essentials.commands;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.IEssentialsModule;
import com.earth2me.essentials.User;
import net.ess3.api.IEssentials;
import org.bukkit.Server;
import java.util.ArrayList;
@@ -10,6 +12,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.HashSet;
public class EssentialsTreeCommand extends EssentialsCommand {
private final Map<String, EssentialsTreeNode> nodes = new HashMap<>();
@@ -32,6 +35,22 @@ public class EssentialsTreeCommand extends EssentialsCommand {
node.setParent(this);
}
@Override
public void setEssentials(final IEssentials ess) {
super.setEssentials(ess);
for (final EssentialsTreeNode node : new HashSet<>(nodes.values())) {
node.setEssentials(ess);
}
}
@Override
public void setEssentialsModule(final IEssentialsModule module) {
super.setEssentialsModule(module);
for (final EssentialsTreeNode node : new HashSet<>(nodes.values())) {
node.setEssentialsModule(module);
}
}
public void runDefault(final User user, final String commandLabel) throws Exception {
runDefault(user.getSource(), commandLabel);
}
@@ -37,7 +37,7 @@ public class VersionCommand extends EssentialsTreeNode {
);
public VersionCommand() {
super("version");
super("version", "ver");
}
@Override
@@ -171,6 +171,16 @@ public class UserConfigHolder {
this.godmode = value;
}
private boolean flymode = false;
public boolean flyMode() {
return this.flymode;
}
public void flyMode(final boolean value) {
this.flymode = value;
}
private boolean muted = false;
public boolean muted() {
@@ -32,7 +32,7 @@ public final class EconomyLayers {
}
public static void onEnable(final Essentials ess) {
ess.scheduleInitTask(() -> {
ess.scheduleSyncDelayedTask(() -> {
serverStarted = true;
for (final Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
if (!plugin.isEnabled()) {
@@ -122,7 +122,12 @@ public interface IMessageRecipient extends MailSender {
/**
* States that the message was <b>NOT</b> received as a result of the message pre-send event being cancelled.
*/
EVENT_CANCELLED;
EVENT_CANCELLED,
/**
* States that the message was <b>NOT</b> delivered as a result of the sender being muted. The message may
* still have been shown to social spies.
*/
SENDER_MUTED;
/**
* Returns whether this response is a success. In other words equal to {@link #SUCCESS} or {@link #SUCCESS_BUT_AFK}
@@ -91,6 +91,18 @@ public class SimpleMessageRecipient implements IMessageRecipient {
}
message = preSendEvent.getMessage();
final User senderUser = getUser(this);
// A muted player must not have their message delivered to the recipient. However, social spies
// may still observe the attempted message (see the socialspy-listen-muted-players setting).
// Note: PrivateMessageSentEvent is intentionally not fired here so the message is not relayed
// elsewhere (e.g. to Discord) as though it had actually been delivered.
if (senderUser != null && senderUser.isMuted()) {
sendSocialSpy(recipient, message, true);
senderUser.notifyMuted();
return MessageResponse.SENDER_MUTED;
}
final MessageResponse messageResponse = recipient.onReceiveMessage(this.parent, message);
switch (messageResponse) {
case UNREACHABLE:
@@ -114,29 +126,7 @@ public class SimpleMessageRecipient implements IMessageRecipient {
sendTl("msgFormat", AdventureUtil.parsed(tlSender("meSender")), recipient.getDisplayName(), message);
// Better Social Spy
if (ess.getSettings().isSocialSpyMessages()) {
final User senderUser = getUser(this);
final User recipientUser = getUser(recipient);
if (senderUser != null // not null if player.
// Dont spy on chats involving socialspy exempt players
&& !senderUser.isAuthorized("essentials.chat.spy.exempt")
&& recipientUser != null && !recipientUser.isAuthorized("essentials.chat.spy.exempt")) {
final String senderName = ess.getSettings().isSocialSpyDisplayNames() ? getDisplayName() : getName();
final String recipientName = ess.getSettings().isSocialSpyDisplayNames() ? recipient.getDisplayName() : recipient.getName();
for (final User onlineUser : ess.getOnlineUsers()) {
if (onlineUser.isSocialSpyEnabled()
// Don't send socialspy messages to message sender/receiver to prevent spam
&& !onlineUser.equals(senderUser)
&& !onlineUser.equals(recipient)) {
if (senderUser.isMuted() && ess.getSettings().getSocialSpyListenMutedPlayers()) {
onlineUser.sendComponent(ess.getAdventureFacet().deserializeMiniMessage(tlSender("socialSpyMutedPrefix") + tlLiteral("socialSpyMsgFormat", senderName, recipientName, message)));
} else {
onlineUser.sendComponent(ess.getAdventureFacet().deserializeMiniMessage(tlLiteral("socialSpyPrefix") + tlLiteral("socialSpyMsgFormat", senderName, recipientName, message)));
}
}
}
}
}
sendSocialSpy(recipient, message, false);
break;
}
// If the message was a success, set this sender's reply-recipient to the current recipient.
@@ -150,6 +140,45 @@ public class SimpleMessageRecipient implements IMessageRecipient {
return messageResponse;
}
/**
* Shows a private message to all online social spies, unless either party is exempt from being spied on.
*
* @param recipient the recipient of the private message
* @param message the message that was sent
* @param muted whether the sender is muted; muted messages are only shown when
* {@code socialspy-listen-muted-players} is enabled and use a distinct prefix
*/
private void sendSocialSpy(final IMessageRecipient recipient, final String message, final boolean muted) {
if (!ess.getSettings().isSocialSpyMessages()) {
return;
}
if (muted && !ess.getSettings().getSocialSpyListenMutedPlayers()) {
return;
}
final User senderUser = getUser(this);
final User recipientUser = getUser(recipient);
// Don't spy on chats involving socialspy exempt players.
if (senderUser == null // not null if player.
|| senderUser.isAuthorized("essentials.chat.spy.exempt")
|| recipientUser == null
|| recipientUser.isAuthorized("essentials.chat.spy.exempt")) {
return;
}
final String senderName = ess.getSettings().isSocialSpyDisplayNames() ? getDisplayName() : getName();
final String recipientName = ess.getSettings().isSocialSpyDisplayNames() ? recipient.getDisplayName() : recipient.getName();
final String prefix = muted ? tlSender("socialSpyMutedPrefix") : tlLiteral("socialSpyPrefix");
for (final User onlineUser : ess.getOnlineUsers()) {
if (onlineUser.isSocialSpyEnabled()
// Don't send socialspy messages to message sender/receiver to prevent spam
&& !onlineUser.equals(senderUser)
&& !onlineUser.equals(recipient)) {
onlineUser.sendComponent(ess.getAdventureFacet().deserializeMiniMessage(prefix + tlLiteral("socialSpyMsgFormat", senderName, recipientName, message)));
}
}
}
@Override
public MessageResponse onReceiveMessage(final IMessageRecipient sender, final String message) {
if (!isReachable()) {
@@ -28,6 +28,13 @@ public interface IPermissionsHandler {
boolean hasPermission(Player base, String node);
default boolean hasPermissionCached(Player base, String node) {
return hasPermission(base, node);
}
default void invalidatePermissionCache(UUID uuid) {
}
// Does not check for * permissions
boolean isPermissionSet(Player base, String node);
@@ -105,6 +105,16 @@ public class PermissionsHandler implements IPermissionsHandler {
return handler.hasPermission(base, node);
}
@Override
public boolean hasPermissionCached(final Player base, final String node) {
return handler.hasPermissionCached(base, node);
}
@Override
public void invalidatePermissionCache(final UUID uuid) {
handler.invalidatePermissionCache(uuid);
}
@Override
public boolean isPermissionSet(final Player base, final String node) {
return handler.isPermissionSet(base, node);
@@ -7,17 +7,22 @@ import net.luckperms.api.context.ContextCalculator;
import net.luckperms.api.context.ContextConsumer;
import net.luckperms.api.context.ContextSet;
import net.luckperms.api.context.ImmutableContextSet;
import net.luckperms.api.event.EventSubscription;
import net.luckperms.api.event.user.UserDataRecalculateEvent;
import net.luckperms.api.model.group.Group;
import net.luckperms.api.query.QueryOptions;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import java.util.logging.Level;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -25,6 +30,8 @@ public class LuckPermsHandler extends ModernVaultHandler {
private LuckPerms luckPerms;
private Essentials ess;
private CombinedCalculator calculator;
private final Map<UUID, Map<String, Boolean>> permissionCache = new ConcurrentHashMap<>();
private EventSubscription<UserDataRecalculateEvent> recalculateSubscription;
@Override
public void registerContext(final String context, final Function<User, Iterable<String>> calculator, final Supplier<Iterable<String>> suggestions) {
@@ -41,6 +48,30 @@ public class LuckPermsHandler extends ModernVaultHandler {
this.luckPerms.getContextManager().unregisterCalculator(this.calculator);
this.calculator = null;
}
if (this.recalculateSubscription != null) {
this.recalculateSubscription.close();
this.recalculateSubscription = null;
}
this.permissionCache.clear();
}
@Override
public boolean hasPermissionCached(final Player base, final String node) {
final UUID uuid = base.getUniqueId();
final Map<String, Boolean> userCache = permissionCache.computeIfAbsent(uuid, k -> new ConcurrentHashMap<>());
return userCache.computeIfAbsent(node, k -> hasPermission(base, node));
}
@Override
public void invalidatePermissionCache(final UUID uuid) {
invalidateCache(uuid);
}
public void invalidateCache(final UUID uuid) {
if (ess.getSettings().isDebug()) {
ess.getLogger().log(Level.INFO, "Invalidating permission cache for " + uuid);
}
permissionCache.remove(uuid);
}
@Override
@@ -62,7 +93,13 @@ public class LuckPermsHandler extends ModernVaultHandler {
@Override
public boolean isOfflinePermissionSet(UUID uuid, String node) {
final net.luckperms.api.model.user.User user = this.luckPerms.getUserManager().loadUser(uuid).join();
// Try to get the user from LuckPerms' cache first to avoid blocking the main
// thread with a database lookup. In the context of a player join (e.g.
// PlayerServerFullCheckEvent), the user should already be loaded.
net.luckperms.api.model.user.User user = this.luckPerms.getUserManager().getUser(uuid);
if (user == null) {
user = this.luckPerms.getUserManager().loadUser(uuid).join();
}
if (user == null) {
return false;
}
@@ -77,6 +114,11 @@ public class LuckPermsHandler extends ModernVaultHandler {
if (provider != null) {
this.luckPerms = provider.getProvider();
this.ess = ess;
this.recalculateSubscription = this.luckPerms.getEventBus().subscribe(
ess,
UserDataRecalculateEvent.class,
event -> invalidateCache(event.getUser().getUniqueId())
);
}
return luckPerms != null && super.tryProvider(ess);
}
@@ -21,10 +21,8 @@ public class SignDisposal extends EssentialsSign {
@Override
protected boolean onSignInteract(final ISign sign, final User player, final String username, final IEssentials ess) {
String title = (sign.getLine(1) + " " + sign.getLine(2) + " " + sign.getLine(3)).trim();
if (title.isEmpty()) {
title = player.playerTl("disposal");
}
final String customTitle = (sign.getLine(1) + " " + sign.getLine(2) + " " + sign.getLine(3)).trim();
final String title = customTitle.isEmpty() ? ess.getAdventureFacet().miniToLegacy(player.playerTl("disposal")) : customTitle;
player.getBase().openInventory(ess.getServer().createInventory(player.getBase(), 36, title));
return true;
}
@@ -97,8 +97,9 @@ public class SignTrade extends EssentialsSign {
stack = stack.clone();
stack.setAmount(amount);
final Trade store = new Trade(stack, ess);
addAmount(sign, 2, store, ess, false);
addAmount(sign, 2, store, ess, true);
store.charge(player);
addAmount(sign, 2, store, ess, false);
return store;
}
}
@@ -288,7 +288,7 @@ public class KeywordReplacer implements IText {
for (final String groupName : playerList.keySet()) {
final List<User> groupUsers = playerList.get(groupName);
if (groupUsers != null && !groupUsers.isEmpty()) {
outputList.put(groupName, PlayerList.listUsers(ess, groupUsers, " "));
outputList.put(groupName, ess.getAdventureFacet().miniToLegacy(PlayerList.listUsers(ess, groupUsers, " ")));
}
}
@@ -346,7 +346,7 @@ public class KeywordReplacer implements IText {
}
break;
case TPS:
replacer = NumberUtil.formatDouble(20d); //todo
replacer = NumberUtil.formatDouble(ess.getTimer().getAverageTPS());
break;
case UPTIME:
replacer = DateUtil.formatDateDiff(ManagementFactory.getRuntimeMXBean().getStartTime());
@@ -380,8 +380,10 @@ public class KeywordReplacer implements IText {
}
if (this.replaceSpacesWithUnderscores) {
// Don't replace spaces with underscores in command nor escape underscores.
if (!line.startsWith("/")) {
// Don't replace spaces with underscores in commands, and skip escaping
// for USERNAME since Minecraft names never contain spaces but can
// contain underscores that must stay literal (e.g. player:{USERNAME}).
if (!line.startsWith("/") && validKeyword != KeywordType.USERNAME) {
replacer = replacer.replace("_", "\\_").replaceAll("\\s", "_");
}
}
@@ -102,6 +102,13 @@ public class ModernUserMap extends CacheLoader<UUID, User> implements IUserMap {
@Override
public User getUser(final Player base) {
final UUID uuid = base.getUniqueId();
final User cachedUser = userCache.getIfPresent(uuid);
if (cachedUser != null && cachedUser.getBase() == base) {
uuidCache.updateCache(uuid, base.getName());
return cachedUser;
}
final User user = loadUncachedUser(base);
userCache.put(user.getUUID(), user);
debugLogCache(user);
@@ -167,6 +167,10 @@ public final class LocationUtil {
return isBlockDamaging(world, x, y, z) || isBlockAboveAir(ess, world, x, y, z);
}
private static boolean isBlockUnsafe(IEssentials ess, final World world, final int x, final int y, final int z, final int maxY) {
return y >= maxY || isBlockUnsafe(ess, world, x, y, z);
}
public static boolean isBlockDamaging(final World world, final int x, final int y, final int z) {
final Material block = world.getBlockAt(x, y, z).getType();
final Material below = world.getBlockAt(x, y - 1, z).getType();
@@ -224,7 +228,9 @@ public final class LocationUtil {
final World world = loc.getWorld();
final int worldMinY = worldInfoProvider.getMinHeight(world);
final int worldLogicalY = worldInfoProvider.getLogicalHeight(world);
final int worldMaxY = loc.getBlockY() < worldLogicalY ? worldLogicalY : worldInfoProvider.getMaxHeight(world);
final int worldMaxY = ess.getSettings().isConsiderWorldHeightForTeleportSafety() && loc.getBlockY() < worldLogicalY
? worldLogicalY
: worldInfoProvider.getMaxHeight(world);
int x = loc.getBlockX();
int y = (int) Math.round(loc.getY());
int z = loc.getBlockZ();
@@ -242,12 +248,12 @@ public final class LocationUtil {
break;
}
}
if (isBlockUnsafe(ess, world, x, y, z)) {
if (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) {
x = Math.round(loc.getX()) == origX ? x - 1 : x + 1;
z = Math.round(loc.getZ()) == origZ ? z - 1 : z + 1;
}
int i = 0;
while (isBlockUnsafe(ess, world, x, y, z)) {
while (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) {
i++;
if (i >= VOLUME.length) {
x = origX;
@@ -259,14 +265,14 @@ public final class LocationUtil {
y = NumberUtil.constrainToRange(origY + VOLUME[i].y, worldMinY, worldMaxY);
z = origZ + VOLUME[i].z;
}
while (isBlockUnsafe(ess, world, x, y, z)) {
while (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) {
y += 1;
if (y >= worldMaxY) {
x += 1;
break;
}
}
while (isBlockUnsafe(ess, world, x, y, z)) {
while (isBlockUnsafe(ess, world, x, y, z, worldMaxY)) {
y -= 1;
if (y <= worldMinY + 1) {
x += 1;
@@ -52,7 +52,7 @@ public final class MaterialUtil {
BEDS = EnumUtil.getAllMatching(Material.class, "BED", "BED_BLOCK", "WHITE_BED", "ORANGE_BED",
"MAGENTA_BED", "LIGHT_BLUE_BED", "YELLOW_BED", "LIME_BED", "PINK_BED", "GRAY_BED",
"LIGHT_GRAY_BED", "CYAN_BED", "PURPLE_BED", "BLUE_BED", "BROWN_BED", "GREEN_BED",
"RED_BED", "BLACK_BED");
"RED_BED", "BLACK_BED", "STRAW_BED");
BANNERS = EnumUtil.getAllMatching(Material.class, "BANNER", "WHITE_BANNER",
"ORANGE_BANNER", "MAGENTA_BANNER", "LIGHT_BLUE_BANNER", "YELLOW_BANNER", "LIME_BANNER",
@@ -86,7 +86,8 @@ public final class MaterialUtil {
"OAK_SIGN", "SPRUCE_SIGN",
"CRIMSON_SIGN", "WARPED_SIGN",
"MANGROVE_SIGN", "CHERRY_SIGN",
"BAMBOO_SIGN", "PALE_OAK_SIGN");
"BAMBOO_SIGN", "PALE_OAK_SIGN",
"POPLAR_SIGN");
WALL_SIGNS = EnumUtil.getAllMatching(Material.class, "WALL_SIGN",
"ACACIA_WALL_SIGN", "BIRCH_WALL_SIGN",
@@ -94,21 +95,24 @@ public final class MaterialUtil {
"OAK_WALL_SIGN", "SPRUCE_WALL_SIGN",
"CRIMSON_WALL_SIGN", "WARPED_WALL_SIGN",
"MANGROVE_WALL_SIGN", "CHERRY_WALL_SIGN",
"BAMBOO_WALL_SIGN", "PALE_OAK_WALL_SIGN");
"BAMBOO_WALL_SIGN", "PALE_OAK_WALL_SIGN",
"POPLAR_WALL_SIGN");
HANGING_SIGNS = EnumUtil.getAllMatching(Material.class, "ACACIA_HANGING_SIGN", "BIRCH_HANGING_SIGN",
"DARK_OAK_HANGING_SIGN", "JUNGLE_HANGING_SIGN",
"OAK_HANGING_SIGN", "SPRUCE_HANGING_SIGN",
"CRIMSON_HANGING_SIGN", "WARPED_HANGING_SIGN",
"MANGROVE_HANGING_SIGN", "CHERRY_HANGING_SIGN",
"BAMBOO_HANGING_SIGN", "PALE_OAK_HANGING_SIGN");
"BAMBOO_HANGING_SIGN", "PALE_OAK_HANGING_SIGN",
"POPLAR_HANGING_SIGN");
HANGING_WALL_SIGNS = EnumUtil.getAllMatching(Material.class, "ACACIA_WALL_HANGING_SIGN", "BIRCH_WALL_HANGING_SIGN",
"DARK_OAK_WALL_HANGING_SIGN", "JUNGLE_WALL_HANGING_SIGN",
"OAK_WALL_HANGING_SIGN", "SPRUCE_WALL_HANGING_SIGN",
"CRIMSON_WALL_HANGING_SIGN", "WARPED_WALL_HANGING_SIGN",
"MANGROVE_WALL_HANGING_SIGN", "CHERRY_WALL_HANGING_SIGN",
"BAMBOO_WALL_HANGING_SIGN", "PALE_OAK_WALL_HANGING_SIGN");
"BAMBOO_WALL_HANGING_SIGN", "PALE_OAK_WALL_HANGING_SIGN",
"POPLAR_WALL_HANGING_SIGN");
}
private MaterialUtil() {
@@ -0,0 +1,23 @@
package com.earth2me.essentials.utils;
import io.papermc.lib.environments.PaperEnvironment;
import io.papermc.lib.features.asyncchunks.AsyncChunksPaper_15;
import io.papermc.lib.features.asyncteleport.AsyncTeleportPaper_13;
import io.papermc.lib.features.bedspawnlocation.BedSpawnLocationPaper;
import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedApiExists;
/**
* A PaperLib environment for Paper servers whose version PaperLib cannot read.
* <p>
* PaperLib 1.0.6 expects a single digit major version in {@code Bukkit#getVersion()}, so it reads 26.x as version 0 and
* selects its synchronous fallbacks for chunk loading, teleporting and bed lookups. All of those exist on every Paper
* version that reports a year based version.
*/
public class ModernPaperEnvironment extends PaperEnvironment {
public ModernPaperEnvironment() {
asyncChunksHandler = new AsyncChunksPaper_15();
asyncTeleportHandler = new AsyncTeleportPaper_13();
isGeneratedHandler = new ChunkIsGeneratedApiExists();
bedSpawnLocationHandler = new BedSpawnLocationPaper();
}
}
@@ -16,12 +16,13 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPOutputStream;
public final class PasteUtil {
private static final String PASTE_URL = "https://pastes.dev/";
private static final String PASTE_UPLOAD_URL = "https://api.pastes.dev/post";
private static final ExecutorService PASTE_EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
private static volatile ExecutorService PASTE_EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
private static final Gson GSON = new Gson();
private PasteUtil() {
@@ -35,7 +36,7 @@ public final class PasteUtil {
*/
public static CompletableFuture<PasteResult> createPaste(List<PasteFile> pages) {
final CompletableFuture<PasteResult> future = new CompletableFuture<>();
PASTE_EXECUTOR_SERVICE.submit(() -> {
getExecutor().submit(() -> {
try {
final HttpURLConnection connection = (HttpURLConnection) new URL(PASTE_UPLOAD_URL).openConnection();
connection.setRequestMethod("POST");
@@ -81,6 +82,28 @@ public final class PasteUtil {
return future;
}
private static synchronized ExecutorService getExecutor() {
if (PASTE_EXECUTOR_SERVICE == null || PASTE_EXECUTOR_SERVICE.isShutdown() || PASTE_EXECUTOR_SERVICE.isTerminated()) {
PASTE_EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
}
return PASTE_EXECUTOR_SERVICE;
}
public static synchronized void shutdownExecutor() {
final ExecutorService exec = PASTE_EXECUTOR_SERVICE;
if (exec == null) {
return;
}
exec.shutdown();
try {
if (!exec.awaitTermination(5, TimeUnit.SECONDS)) {
exec.shutdownNow();
}
} catch (final InterruptedException ignored) {
exec.shutdownNow();
}
}
public static class PasteFile {
private final String name;
private final String contents;
@@ -18,11 +18,21 @@ public final class StringUtil {
private StringUtil() {
}
private static final Pattern WINDOWS_RESERVED = Pattern.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$");
//Used to clean file names before saving to disk
public static String sanitizeFileName(final String name) {
return INVALIDFILECHARS.matcher(name.toLowerCase(Locale.ENGLISH)).replaceAll("_");
}
/**
* Returns true if the given name is a Windows reserved filename (e.g. CON, NUL, AUX).
* Using these as file names on Windows will hang or crash the server.
*/
public static boolean isReservedFileName(final String name) {
return WINDOWS_RESERVED.matcher(name.toLowerCase(Locale.ENGLISH)).matches();
}
//Used to clean strings/names before saving as filenames/permissions
public static String safeString(final String string) {
if (string == null) {
@@ -40,17 +40,20 @@ public final class VersionUtil {
public static final BukkitVersion v1_19_R01 = BukkitVersion.fromString("1.19-R0.1-SNAPSHOT");
public static final BukkitVersion v1_19_4_R01 = BukkitVersion.fromString("1.19.4-R0.1-SNAPSHOT");
public static final BukkitVersion v1_20_1_R01 = BukkitVersion.fromString("1.20.1-R0.1-SNAPSHOT");
public static final BukkitVersion v1_20_4_R01 = BukkitVersion.fromString("1.20.4-R0.1-SNAPSHOT");
public static final BukkitVersion v1_20_6_R01 = BukkitVersion.fromString("1.20.6-R0.1-SNAPSHOT");
public static final BukkitVersion v1_21_R01 = BukkitVersion.fromString("1.21-R0.1-SNAPSHOT");
public static final BukkitVersion v1_21_3_R01 = BukkitVersion.fromString("1.21.3-R0.1-SNAPSHOT");
public static final BukkitVersion v1_21_5_R01 = BukkitVersion.fromString("1.21.5-R0.1-SNAPSHOT");
public static final BukkitVersion v1_21_8_R01 = BukkitVersion.fromString("1.21.8-R0.1-SNAPSHOT");
public static final BukkitVersion v1_21_11_R01 = BukkitVersion.fromString("1.21.11-R0.1-SNAPSHOT");
public static final BukkitVersion v26_1_R01 = BukkitVersion.fromString("26.1.2-R0.1-SNAPSHOT");
public static final BukkitVersion v26_2_R01 = BukkitVersion.fromString("26.2-R0.1-SNAPSHOT");
public static final BukkitVersion v26_3_R01 = BukkitVersion.fromString("26.3-R0.1-SNAPSHOT");
private static final Set<BukkitVersion> supportedVersions = ImmutableSet.of(v1_8_8_R01, v1_9_4_R01, v1_10_2_R01, v1_11_2_R01, v1_12_2_R01, v1_13_2_R01, v1_14_4_R01, v1_15_2_R01, v1_16_5_R01, v1_17_1_R01, v1_18_2_R01, v1_19_4_R01, v1_20_6_R01, v1_21_11_R01);
private static final Set<BukkitVersion> supportedVersions = ImmutableSet.of(v1_8_8_R01, v1_9_4_R01, v1_10_2_R01, v1_11_2_R01, v1_12_2_R01, v1_13_2_R01, v1_14_4_R01, v1_15_2_R01, v1_16_5_R01, v1_17_1_R01, v1_18_2_R01, v1_19_4_R01, v1_20_6_R01, v1_21_11_R01, v26_1_R01, v26_2_R01, v26_3_R01);
public static final boolean PRE_FLATTENING = VersionUtil.getServerBukkitVersion().isLowerThan(VersionUtil.v1_13_0_R01);
public static final boolean FOLIA;
public static final List<String> officialPlugins = Arrays.asList(
"EssentialsAntiBuild",
@@ -76,15 +79,6 @@ public final class VersionUtil {
private static final String PFX = make("8(;4>`");
static {
boolean isFolia;
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
isFolia = true;
} catch (Throwable ignored) {
isFolia = false;
}
FOLIA = isFolia;
final ImmutableMap.Builder<String, SupportStatus> builder = new ImmutableMap.Builder<>();
// Leaf, yet another "High Performance" fork of Paper. Not supported by EssentialsX.
@@ -185,7 +179,7 @@ public final class VersionUtil {
}
}
if (!supportedVersions.contains(getServerBukkitVersion())) {
if (!isSupportedVersion(getServerBukkitVersion())) {
return supportStatus = SupportStatus.OUTDATED;
}
@@ -198,14 +192,39 @@ public final class VersionUtil {
return supportStatusClass;
}
/**
* Checks if a version is considered supported, either by exact match in the
* supported versions set, or by matching the base version (major.minor.patch)
* of a supported version. This handles both the new PaperMC versioning scheme
* (which omits the Bukkit revision suffix) and development builds that report
* versions like {@code 26.1-rc-3.build.8-alpha}.
*/
private static boolean isSupportedVersion(final BukkitVersion version) {
if (supportedVersions.contains(version)) {
return true;
}
// Check if this version matches a supported base version.
// This covers Paper versions (no -R0.1-SNAPSHOT suffix) and
// dev variants (snapshot/pre-release/RC/Paper builds).
for (final BukkitVersion supported : supportedVersions) {
if (version.equalsBaseVersion(supported)) {
return true;
}
}
return false;
}
public static final class BukkitVersion implements Comparable<BukkitVersion> {
private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.?([0-9]*)?(?:-pre(\\d))?(?:-rc(\\d+))?(?:-?R?([\\d.]+))?(?:-SNAPSHOT)?");
private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("^(\\d{2})w(\\d{2})([a-z])(?:-?R?([\\d.]+))?(?:-SNAPSHOT)?");
private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.?([0-9]*)?(?:-snapshot-(\\d+))?(?:-pre-?(\\d+))?(?:-rc-?(\\d+))?(?:\\.build\\.(\\d+)(?:-([a-z]+))?)?(?:-?R?([\\d.]+))?(?:(?:\\.local)?-SNAPSHOT)?");
private static final Pattern LEGACY_SNAPSHOT_PATTERN = Pattern.compile("^(\\d{2})w(\\d{2})([a-z])(?:-?R?([\\d.]+))?(?:-SNAPSHOT)?");
private final int major;
private final int minor;
private final int snapshotRelease;
private final int preRelease;
private final int releaseCandidate;
private final int paperBuild;
private final String releaseChannel;
private final int patch;
private final double revision;
@@ -214,27 +233,33 @@ public final class VersionUtil {
private final int snapshotWeek;
private final char snapshotLetter;
private BukkitVersion(final int major, final int minor, final int patch, final double revision, final int preRelease, final int releaseCandidate) {
private BukkitVersion(final int major, final int minor, final int patch, final double revision, final int snapshotRelease, final int preRelease, final int releaseCandidate, final int paperBuild, final String releaseChannel) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.revision = revision;
this.snapshotRelease = snapshotRelease;
this.preRelease = preRelease;
this.releaseCandidate = releaseCandidate;
this.paperBuild = paperBuild;
this.releaseChannel = releaseChannel;
this.snapshot = false;
this.snapshotYear = -1;
this.snapshotWeek = -1;
this.snapshotLetter = '\0';
}
private BukkitVersion(final int major, final int minor, final int patch, final double revision, final int preRelease, final int releaseCandidate,
private BukkitVersion(final int major, final int minor, final int patch, final double revision, final int snapshotRelease, final int preRelease, final int releaseCandidate, final int paperBuild, final String releaseChannel,
final boolean snapshot, final int snapshotYear, final int snapshotWeek, final char snapshotLetter) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.revision = revision;
this.snapshotRelease = snapshotRelease;
this.preRelease = preRelease;
this.releaseCandidate = releaseCandidate;
this.paperBuild = paperBuild;
this.releaseChannel = releaseChannel;
this.snapshot = snapshot;
this.snapshotYear = snapshotYear;
this.snapshotWeek = snapshotWeek;
@@ -247,11 +272,11 @@ public final class VersionUtil {
// Try standard release format first
Matcher matcher = VERSION_PATTERN.matcher(string);
if (matcher.matches()) {
return from(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(6), matcher.group(4), matcher.group(5));
return from(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(9), matcher.group(4), matcher.group(5), matcher.group(6), matcher.group(7), matcher.group(8));
}
// Try snapshot format (e.g., 25w32a-R0.1-SNAPSHOT)
final Matcher snapshotMatcher = SNAPSHOT_PATTERN.matcher(string);
final Matcher snapshotMatcher = LEGACY_SNAPSHOT_PATTERN.matcher(string);
if (snapshotMatcher.matches()) {
final int year = Integer.parseInt(snapshotMatcher.group(1));
final int week = Integer.parseInt(snapshotMatcher.group(2));
@@ -269,24 +294,29 @@ public final class VersionUtil {
}
matcher = VERSION_PATTERN.matcher(v1_16_1_R01.toString());
Preconditions.checkArgument(matcher.matches(), string + " is not in valid version format. e.g. 1.8.8-R0.1");
return from(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(6), matcher.group(4), matcher.group(5));
return from(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(9), matcher.group(4), matcher.group(5), matcher.group(6), matcher.group(7), matcher.group(8));
}
private static BukkitVersion from(final String major, final String minor, String patch, String revision, String preRelease, String releaseCandidate) {
private static BukkitVersion from(final String major, final String minor, String patch, String revision, String snapshotRelease, String preRelease, String releaseCandidate, String paperBuild, final String releaseChannel) {
if (patch == null || patch.isEmpty()) patch = "0";
if (revision == null || revision.isEmpty()) revision = "0";
if (snapshotRelease == null || snapshotRelease.isEmpty()) snapshotRelease = "-1";
if (preRelease == null || preRelease.isEmpty()) preRelease = "-1";
if (releaseCandidate == null || releaseCandidate.isEmpty()) releaseCandidate = "-1";
if (paperBuild == null || paperBuild.isEmpty()) paperBuild = "-1";
return new BukkitVersion(Integer.parseInt(major),
Integer.parseInt(minor),
Integer.parseInt(patch),
Double.parseDouble(revision),
Integer.parseInt(snapshotRelease),
Integer.parseInt(preRelease),
Integer.parseInt(releaseCandidate));
Integer.parseInt(releaseCandidate),
Integer.parseInt(paperBuild),
releaseChannel);
}
private static BukkitVersion fromSnapshot(final int year, final int week, final char letter, final double revision) {
return new BukkitVersion(-1, -1, -1, revision, -1, -1, true, year, week, letter);
return new BukkitVersion(-1, -1, -1, revision, -1, -1, -1, -1, null, true, year, week, letter);
}
public boolean isHigherThan(final BukkitVersion o) {
@@ -329,10 +359,37 @@ public final class VersionUtil {
return releaseCandidate;
}
public int getSnapshotRelease() {
return snapshotRelease;
}
public int getPaperBuild() {
return paperBuild;
}
public String getReleaseChannel() {
return releaseChannel;
}
public boolean isSnapshot() {
return snapshot;
}
/**
* Checks if this version has the same base version (major, minor, and patch)
* as the given version, ignoring development specifiers and Bukkit metadata.
* Revision is ignored because Paper versions do not include the Bukkit
* revision suffix (e.g. {@code -R0.1-SNAPSHOT}).
*/
public boolean equalsBaseVersion(final BukkitVersion other) {
if (this.snapshot || other.snapshot) {
return false;
}
return this.major == other.major &&
this.minor == other.minor &&
this.patch == other.patch;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
@@ -353,7 +410,9 @@ public final class VersionUtil {
minor == that.minor &&
patch == that.patch &&
revision == that.revision &&
preRelease == that.preRelease;
snapshotRelease == that.snapshotRelease &&
preRelease == that.preRelease &&
releaseCandidate == that.releaseCandidate;
}
@Override
@@ -361,7 +420,7 @@ public final class VersionUtil {
if (snapshot) {
return Objects.hashCode("snapshot", snapshotYear, snapshotWeek, snapshotLetter, revision);
}
return Objects.hashCode(major, minor, patch, revision, preRelease, releaseCandidate);
return Objects.hashCode(major, minor, patch, revision, snapshotRelease, preRelease, releaseCandidate);
}
@Override
@@ -373,15 +432,31 @@ public final class VersionUtil {
if (patch != 0) {
sb.append(".").append(patch);
}
if (snapshotRelease != -1) {
sb.append("-snapshot-").append(snapshotRelease);
}
if (preRelease != -1) {
sb.append("-pre").append(preRelease);
sb.append("-pre-").append(preRelease);
}
if (releaseCandidate != -1) {
sb.append("-rc").append(releaseCandidate);
sb.append("-rc-").append(releaseCandidate);
}
if (paperBuild != -1) {
sb.append(".build.").append(paperBuild);
if (releaseChannel != null) {
sb.append("-").append(releaseChannel);
}
}
return sb.append("-R").append(revision).toString();
}
private static int compareDevSpecifier(final int a, final int b) {
if (a == b) return 0;
if (a == -1) return 1;
if (b == -1) return -1;
return Integer.compare(a, b);
}
@Override
public int compareTo(final BukkitVersion o) {
// Snapshots are always considered the most recent
@@ -417,18 +492,20 @@ public final class VersionUtil {
} else if (patch > o.patch) {
return 1;
} else { // equal patch
if (preRelease < o.preRelease) {
// For dev specifiers, -1 means absent (final release) and
// sorts HIGHER than any present value: snapshot < pre < rc < release
int cmp = compareDevSpecifier(snapshotRelease, o.snapshotRelease);
if (cmp != 0) return cmp;
cmp = compareDevSpecifier(preRelease, o.preRelease);
if (cmp != 0) return cmp;
cmp = compareDevSpecifier(releaseCandidate, o.releaseCandidate);
if (cmp != 0) return cmp;
if (paperBuild < o.paperBuild) {
return -1;
} else if (preRelease > o.preRelease) {
} else if (paperBuild > o.paperBuild) {
return 1;
} else { // equal prerelease
if (releaseCandidate < o.releaseCandidate) {
return -1;
} else if (releaseCandidate > o.releaseCandidate) {
return 1;
} else { // equal release candidate
return Double.compare(revision, o.revision);
}
} else {
return Double.compare(revision, o.revision);
}
}
}
@@ -8,6 +8,12 @@ import org.bukkit.event.HandlerList;
* <p>
* For other cases where the player's vanish status changes, you should listen on PlayerJoinEvent and
* check with {@link IUser#isVanished()}.
*
* <b>WARNING: The values of {@link #getAffected()} and {@link #getController()} are inverted due to
* a long-standing parameter swap. {@link #getAffected()} returns the command sender (null for console),
* and {@link #getController()} returns the player whose vanish status changed.</b>
*
* @see <a href="https://github.com/EssentialsX/Essentials/issues/2604">#2604</a>
*/
public class VanishStatusChangeEvent extends StatusChangeEvent {
private static final HandlerList handlers = new HandlerList();
@@ -13,11 +13,13 @@ public class AsyncUserDataLoadEvent extends Event {
private final IUser user;
private final String joinMessage;
private final boolean firstJoin;
public AsyncUserDataLoadEvent(IUser user, String joinMessage) {
public AsyncUserDataLoadEvent(IUser user, String joinMessage, boolean firstJoin) {
super(true);
this.user = user;
this.joinMessage = joinMessage;
this.firstJoin = firstJoin;
}
/**
@@ -34,6 +36,18 @@ public class AsyncUserDataLoadEvent extends Event {
return joinMessage;
}
/**
* Whether this is the first time EssentialsX has seen this player join.
* <p>
* This is determined from EssentialsX's own user data rather than {@link org.bukkit.OfflinePlayer#hasPlayedBefore()},
* which is unreliable on modern server platforms that persist player data during the login/configuration phase.
*
* @return {@code true} if this is the player's first recorded join, otherwise {@code false}.
*/
public boolean isFirstJoin() {
return firstJoin;
}
@Override
public HandlerList getHandlers() {
return handlers;
@@ -0,0 +1,59 @@
package net.essentialsx.api.v2.events;
import com.earth2me.essentials.CommandSource;
import com.google.common.base.Preconditions;
import net.ess3.api.IUser;
import org.bukkit.Bukkit;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import java.math.BigDecimal;
/**
* Fired when a transaction (e.g. /pay) is about to be handled.
*/
public class PreTransactionEvent extends TransactionEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private boolean cancelled;
public PreTransactionEvent(final CommandSource requester, final IUser target, final BigDecimal amount) {
super(!Bukkit.isPrimaryThread(), requester, target, amount);
}
/**
* Sets the amount to be subtracted from the requester's balance.
* <p>
* Note: Changing this amount will not verify the requester actually has enough balance to complete the transaction.
* @param decimal the new amount
*/
public void setAmount(final BigDecimal decimal) {
Preconditions.checkNotNull(decimal, "decimal cannot be null");
Preconditions.checkArgument(decimal.compareTo(BigDecimal.ZERO) >= 0, "decimal cannot be negative");
this.amount = decimal;
}
@Override
public boolean isCancelled() {
return cancelled;
}
/**
* If this event should be cancelled. If cancelled, no messages will be displayed to the users involved.
* @param cancelled whether this event should be cancelled
*/
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
@@ -16,15 +16,19 @@ public class TransactionEvent extends Event {
private final CommandSource requester;
private final IUser target;
private final BigDecimal amount;
protected BigDecimal amount;
public TransactionEvent(CommandSource requester, IUser target, BigDecimal amount) {
super(!Bukkit.isPrimaryThread());
protected TransactionEvent(boolean async, CommandSource requester, IUser target, BigDecimal amount) {
super(async);
this.requester = requester;
this.target = target;
this.amount = amount;
}
public TransactionEvent(CommandSource requester, IUser target, BigDecimal amount) {
this(!Bukkit.isPrimaryThread(), requester, target, amount);
}
/**
* @return the user who initiated the transaction
*/
+79 -4
View File
@@ -34,7 +34,7 @@ ops-name-color: '4'
nickname-prefix: '~'
# The maximum length allowed in nicknames. The nickname prefix is not included in this.
max-nick-length: 15
max-nick-length: 16
# The regex pattern used to determine if a requested nickname should be allowed for use.
# If the requested nickname does not match this pattern, the nickname will be rejected.
@@ -51,6 +51,10 @@ nick-blacklist:
# For example, if "&6Notch" has 7 characters (2 are part of a color code), a length of 5 is used when this option is set to true.
ignore-colors-in-max-nick-length: false
# When this option is enabled, any nickname set by a player will be reset when their Minecraft account name changes.
# This prevents old nicknames (including RGB-formatted ones) from persisting after a name change.
reset-nick-on-name-change: false
# When this option is enabled, display names for hidden players will not be shown. This prevents players from being
# able to see that they are online while vanished.
hide-displayname-in-vanish: true
@@ -95,6 +99,10 @@ force-disable-teleport-safety: false
# safe location. If you'd like players to be teleported to a safe location all of the time, set this option to true.
force-safe-teleport-location: false
# When teleporting to an unsafe location, should Essentials consider the world's logical height when finding a safe destination?
# This prevents safety checks from moving players above the Nether roof unless the requested destination is already above it.
consider-world-height-for-teleport-safety: true
# Consider water blocks as "safe", therefore allowing players to teleport
# using commands such as /home or /spawn to a location that is occupied by water blocks.
is-water-safe: false
@@ -491,7 +499,7 @@ auto-afk: 300
auto-afk-timeout: -1
# A list of commands to be executed instead of kicking the player once the
# threshold defined above in 'afk-auto-timeout' is reached. If this list is empty
# threshold defined above in 'afk-auto-timeout' is reached. If this list is empty
# and 'afk-auto-timeout' is not set to -1, Essentials will default to
# kicking the player once they reach the timeout threshold.
#
@@ -527,6 +535,9 @@ cancel-afk-on-move: true
# Should Essentials automatically remove AFK status when a player sends a chat message?
cancel-afk-on-chat: true
# Should Essentials automatically remove AFK status when a player uses a fishing rod?
cancel-afk-on-fish: true
# Should AFK players be ignored when other players are trying to sleep?
# When this setting is false, players won't be able to skip the night if some players are AFK.
# Players with the permission 'essentials.sleepingignored' will always be ignored.
@@ -897,8 +908,8 @@ baltop-requirements:
minimum-balance: 0
minimum-playtime: 0
# Limit the number of cached balance top entries.
# Recommended for servers with a large number of players, as it reduces memory usage.
# Limit the number of cached balance top entries.
# Recommended for servers with a large number of players, as it reduces memory usage.
# Set to -1 to disable the limit.
baltop-entry-limit: -1
@@ -1138,6 +1149,70 @@ protect:
witch: false
horse: false
phantom: false
zombified_piglin: false
mooshroom: false
snow_golem: false
rabbit: false
guardian: false
elder_guardian: false
endermite: false
shulker: false
polar_bear: false
stray: false
husk: false
wither_skeleton: false
zombie_villager: false
skeleton_horse: false
zombie_horse: false
donkey: false
mule: false
llama: false
evoker: false
vex: false
vindicator: false
illusioner: false
parrot: false
turtle: false
cod: false
salmon: false
pufferfish: false
tropical_fish: false
dolphin: false
drowned: false
cat: false
fox: false
panda: false
pillager: false
ravager: false
trader_llama: false
wandering_trader: false
bee: false
hoglin: false
piglin: false
piglin_brute: false
strider: false
zoglin: false
axolotl: false
glow_squid: false
goat: false
allay: false
frog: false
tadpole: false
warden: false
camel: false
sniffer: false
armadillo: false
bogged: false
breeze: false
creaking: false
happy_ghast: false
copper_golem: false
camel_husk: false
nautilus: false
zombie_nautilus: false
parched: false
sulfur_cube: false
cushion: false
# The maximum height a creeper can explode. -1 allows them to explode everywhere.
# Set 'creeper-explosion' above to true if you want to disable creeper explosions completely.
File diff suppressed because it is too large. Load diff
Loaded 100 of 202 files, more files were not shown because too many files have changed in this diff. Show more