Compare commits

...
Author SHA1 Message Date
Martin Sulikowski bc11f46275 Respect ignored worlds for void combat 2026-08-17 21:32:18 +02:00
Martin Sulikowski 35e19d0eed Keep combat tag active below void height to prevent evasion 2026-08-17 21:24:24 +02:00
renovate[bot] b2776936ed dependency: Update junit-framework monorepo to v6.1.3 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 20:58:09 +02:00
Martin Sulikowski d2aa5dfa84 GH-395 Add combat sign editing blocker (#395)
* Add combat sign editing blocker

* Fix.
2026-08-17 20:57:17 +02:00
renovate[bot] 5c88af692d dependency: Update Gradle to v9.7.0 (#419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 20:50:56 +02:00
renovate[bot] 390195edb2 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.6.1 (#413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 10:11:56 +02:00
renovate[bot] 8808331f0a dependency: Update junit-framework monorepo to v6.1.2 (#411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 17:14:08 +02:00
renovate[bot] 99b3a5aa27 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.4.10 (#412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 17:14:00 +02:00
renovate[bot] 9d1716d9db dependency: Update Gradle to v9.6.1 (#405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:52:33 +02:00
renovate[bot] 7b94ab4b7a dependency: Update Versions.PACKETS_EVENTS (#402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:52:13 +02:00
renovate[bot] daee189865 dependency: Update dependency me.clip:placeholderapi to v2.12.3 (#407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:59 +02:00
renovate[bot] 1aef1e6f1d dependency: Update junit-framework monorepo to v6.1.1 (#408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:49 +02:00
renovate[bot] 788a2a668c dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.5.1 (#409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:41 +02:00
Nico SendanandMartin Sulikowski 93d7ffdd0f GH-400 Resolve EntityMountEvent registration failure on modern Paper (#400)
* Fix: Resolve EntityMountEvent registration failure on modern Paper

Newer Paper builds (e.g. 26.1.2) removed org.spigotmc.event.entity.EntityMountEvent, causing KnockbackRegionController registration to fail and region knockback protection to be disabled.

Resolve EntityMountEvent at runtime (checking Bukkit, Paper, and Spigot packages) and register it reflectively while preserving the 1.19.3 compile target for compatibility.

* Optimize dynamic mount event handling

---------

Co-authored-by: Martin Sulikowski <vluckyyy.biznes@gmail.com>
2026-06-21 15:30:55 +02:00
renovate[bot] fb6db7a008 dependency: Update Gradle to v9.6.0 (#397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:14:16 +02:00
renovate[bot] 7d8a01b7d7 dependency: Update Versions.LITE_COMMANDS to v3.11.0 (#398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:13:50 +02:00
renovate[bot] 08c7482cee dependency: Update actions/checkout action to v7 (#399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:12:36 +02:00
7e24e5afa6 GH-266 New features for Trident and Pearls (#303)
* I've added new options to Pearl and Trident that reset the timer when we use them

* I made the corrections that gemini noticed

* The improvements suggested by the team have been implemented

* Implemented suggestions from the team

* Make tridents throw check work on all tridents, regardless of enchantment

* Implemented suggestions from the team and fixed `FightTridentController` logic

* Corrected comments and field names

* Implemented CitralFlo suggestions

* Implemented CitralFlo suggestions again

* Follow igoyek's review from 21.10.25

* CR

* Simplify naming - extract logic to service for Pearls

* make it more logical

* Refactor trident-related classes and services for improved clarity and consistency

* Rename trident cooldown methods to improve clarity and update comments for riptide usage

* Fixes after tests - everything should be working now!

* Remove commented line

* Add cooldown to enderpearl - hack by:https://www.spigotmc.org/threads/modify-vanilla-ender-pearl-cooldown.629232/

* Bump minecraft version in runServer task to 1.21.11

* Add combat restrictions for Riptide tridents and improve event handling

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/trident/TridentSettings.java

* Apply suggestion from @Jakubk15

* Migrate to commons delay

* Resolve conflicts

* CR

* resolve merge

* resolve merge conflicts and update

---------

Co-authored-by: Jakubk15 <77227023+Jakubk15@users.noreply.github.com>
Co-authored-by: Michał Wojtas <wojtas.michal90@gmail.com>
Co-authored-by: Rollczi <ndejlich5@gmail.com>
Co-authored-by: Martin Sulikowski <vluckyyy.biznes@gmail.com>
2026-06-07 22:36:05 +02:00
bb130ae10e GH-373 Add opponent placeholder in combat notification (#373)
* Add opponent placeholder in combat notification

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/FightTask.java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Norbert Dejlich <ndejlich5@gmail.com>
2026-06-07 21:46:10 +02:00
Igor MichalskiandRollczi 2c5b007d02 GH-331 Add pearl teleport (#331)
* Add pearl stasis prevention feature

Took 27 minutes

* Add message to pearl stasis prevention feature

Took 8 minutes

* CR

---------

Co-authored-by: Rollczi <ndejlich5@gmail.com>
2026-06-07 21:11:41 +02:00
Martin Sulikowski 51b365df1b bump version to 2.5.2-SNAPSHOT for further development 2026-06-06 21:25:32 +02:00
Martin Sulikowski 4f23105ba1 Release: bump version to 2.5.1 for official release 2026-06-06 21:21:25 +02:00
Martin Sulikowski d0c221c184 Fix: Add PlaceholderAPI dependency and ensure safe API initialization/deinitialization 2026-06-06 21:18:29 +02:00
Martin Sulikowski bf529dce32 Readme: Extend supported Minecraft versions to 26.1.2 2026-06-06 02:04:05 +02:00
Martin Sulikowski 74ec795581 Fix: bump version to 2.5.1-SNAPSHOT for further development 2026-06-06 02:03:30 +02:00
Martin Sulikowski b3550110c8 bump version to 2.5.1-SNAPSHOT for further development 2026-06-06 01:57:48 +02:00
Martin Sulikowski 326f7363f1 bump version to 2.5.0 for release 2026-06-06 01:56:00 +02:00
Martin Sulikowski 5645f0473b Modularize build scripts for publishing and run configurations 2026-06-06 01:52:57 +02:00
Martin Sulikowski d0336441ce GH-389 Use paper native, support 26+ minecraft versions. (#389)
* refactor: migrate plugin to modern Paper API

Switch build metadata to Paper plugin descriptors and paper-api from 1.19.3 onward.

Use native Paper Adventure audiences, async chunk loading, and teleport APIs without relocating Adventure.

Add repository guidance and bug report notes.

* Fix.

* fix: disable config cache for paper plugin descriptor task

The plugin-yml paper generator reads Task.project during task execution, which Gradle 9.5 rejects when storing configuration cache state.

Declare the generated descriptor task incompatible with configuration cache so CI can complete while keeping cache enabled for the rest of the build.

* fix: use paper-compatible multification notices

Replace BukkitNotice builders with core Notice builders after moving to multification-paper.

Use the Adventure sound resolver so config serialization no longer depends on multification-bukkit classes.

* fix: resolve combat potion effects through XSeries

Use XPotion for default combat effects so removed Bukkit enum constants such as DAMAGE_RESISTANCE do not crash on newer Paper versions.

Guard plugin disable cleanup when enable fails before the API provider is initialized.

* Fix.

* fix: support modern Paper without Adventure 5

* fix: update EternalCode Commons

* fix: downgrade Java toolchain to 21 and set custom launcher for runServer task
2026-06-06 01:44:12 +02:00
renovate[bot] bc6df07609 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.4.2 (#384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:46:18 +02:00
renovate[bot] 440f77e387 dependency: Update Versions.CAFFEINE to v3.2.4 (#380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:46:04 +02:00
renovate[bot] 3dc115c1f6 dependency: Update Gradle to v9.5.1 (#381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:45:57 +02:00
renovate[bot] 591ba20221 dependency: Update Versions.XSERIES to v13.8.0 (#382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:45:50 +02:00
renovate[bot] 4d2d2222aa dependency: Update junit-framework monorepo to v6.1.0 (#383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:45:39 +02:00
renovate[bot] 62975caf58 dependency: Update Versions.WORLD_GUARD_BUKKIT to v7.0.17 (#385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:45:29 +02:00
renovate[bot] d7c7ac2916 dependency: Update Versions.PACKETS_EVENTS to v2.12.2 (#386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:45:22 +02:00
renovate[bot] d250a6a720 dependency: Update kotlin monorepo to v2.4.0 (#387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 00:45:17 +02:00
Joël | NoPermissionandRollczi 5c6ac79579 GH-374 fix(combat): patch elytra escape exploit (#374)
* - Changed old methods to new worldguard methods

* fix(knockback): prevent roof teleport exploit & improve safe knockback handling

Fixed an issue where players using fight charge could remain inside knockback regions
and get teleported onto the map roof (above playable area).

### Key changes:
- Reworked knockback direction to push players toward nearest region edge
  instead of center-based vector
- Added velocity dampening system for more consistent knockback behavior
- Improved vertical handling:
  - Separate ground vs air vertical values
  - Prevent excessive vertical stacking while airborne

### Teleport fallback system:
- Introduced optional smart fallback teleport when knockback fails
- Added continuous velocity check before teleporting (prevents false triggers)
- Prevent duplicate fallback tasks using active tracking set
- Improved force knockback logic with edge distance + velocity validation

### Safe location handling:
- Implemented safe ground detection system:
  - Avoid unsafe blocks (lava, cactus, magma, etc.)
  - Ensure 2-block air clearance above ground
- Added configurable fallback scanning from highest block
- Prevent teleport if no safe location is found (optional)

### Stability improvements:
- Added recursion limit for region expansion to prevent infinite loops
- Improved region overlap handling during location generation
- Reduced unnecessary teleports and edge-case glitches

### Config additions:
- vertical / maxAirVertical
- dampenVelocity / dampenFactor
- useTeleport (smart fallback)
- safeGroundCheck + safeHighestFallback
- unsafeGroundBlocks
- groundOffset
- maxAttempts
- cancelIfNoSafeGround

Result:
- Eliminates roof teleport exploit
- More natural and predictable knockback
- Safer teleport fallback system
- Better handling of edge cases and overlapping regions

* fix(combat): prevent elytra escape exploit & improve glide restriction handling

Fixed an issue where players could abuse elytra (e.g. cliff jumping, trident launching, or mid-air tagging) to escape combat and bypass restrictions.

### Key changes:
- Fully disabled elytra usage during combat
- Blocked glide activation via EntityToggleGlideEvent
- Added continuous glide checks to stop mid-air abuse
- Forced downward velocity to prevent spacebar glide spam

### Elytra handling system:
- Introduced automatic elytra unequip on combat tag
- Prevents players from staying airborne when entering combat
- Moves elytra safely to inventory or drops if full
- Eliminates pre-glide and mid-air tagging exploits

### Inventory protection:
- Blocked equipping elytra during combat:
  - Click equip
  - Shift-click
  - Hotbar swap (number keys)
- Prevents all known re-equip bypass methods

### Flight & movement improvements:
- Disabled flight properly on combat start
- Prevents glide reactivation while moving

### Messaging improvements:
- Added dedicated elytra combat message
- Clear feedback when elytra is blocked or removed

### Config additions:
- unequipElytraOnCombat
- blockElytraEquipDuringCombat
- elytraDisabledDuringCombat

Result:
- Eliminates cliff jump & trident launch exploits
- Prevents all elytra-based combat escapes
- Provides consistent and predictable combat behavior
- Matches behavior of high-end PvP combat systems

* CR

* CR 2

---------

Co-authored-by: Rollczi <ndejlich5@gmail.com>
2026-06-05 19:48:47 +02:00
Martin Sulikowski 59f0612405 GH-328 Add /combatlog stats command and related permissions and messages. (#306) 2026-05-07 20:01:54 +02:00
renovate[bot] 84d1d5aeb6 dependency: Update Gradle to v9.5.0 (#378)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 20:00:54 +02:00
renovate[bot] e73864923f dependency: Update dependency com.github.retrooper:packetevents-spigot to v2.12.1 (#376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 20:00:47 +02:00
renovate[bot] 195e45ed34 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.3.21 (#377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 20:00:39 +02:00
imgbot[bot]andImgBotApp 459ff06f18 [ImgBot] Optimize images (#361)
*Total -- 20,587.83kb -> 17,156.76kb (16.67%)

/assets/lightning.gif -- 4,208.49kb -> 2,771.07kb (34.16%)
/assets/flare.gif -- 6,525.52kb -> 5,502.98kb (15.67%)
/assets/readme-banner.png -- 112.85kb -> 98.39kb (12.81%)
/assets/border.gif -- 6,004.89kb -> 5,373.86kb (10.51%)
/assets/combatlog.gif -- 2,949.14kb -> 2,658.24kb (9.86%)
/assets/crystals.gif -- 786.95kb -> 752.21kb (4.41%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2026-04-19 20:59:49 +02:00
renovate[bot] 4a153d5557 dependency: Update actions/github-script action to v9 (#372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 11:07:04 +02:00
renovate[bot] 3e5b060923 dependency: Update dependency com.github.retrooper:packetevents-spigot to v2.12.0 (#370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 11:06:38 +02:00
renovate[bot] 03768ccd8d dependency: Update dependency com.sk89q.worldguard:worldguard-bukkit to v7.0.16 (#368)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 11:06:14 +02:00
renovate[bot] ba59d85996 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.4.1 (#369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 11:06:04 +02:00
renovate[bot] 1c432cbcb3 dependency: Update dependency com.github.angeschossen:LandsAPI to v7.25.4 (#366)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 11:03:33 +02:00
renovate[bot] de6b76526c dependency: Update gradle/actions action to v6 (#367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 11:03:23 +02:00
renovate[bot] 7de452ac15 dependency: Update Gradle to v9.4.1 (#365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 09:31:52 +01:00
renovate[bot]andJakub Kędziora 7e578d63f7 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.4.0 (#363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2026-03-22 09:31:44 +01:00
renovate[bot] e5f86c53a2 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.3.20 (#364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 09:31:15 +01:00
renovate[bot] 22260e87d0 dependency: Update plugin com.modrinth.minotaur to v2.9.0 (#359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 19:06:51 +01:00
fe73466cfb GH-352 Add Arc Riders style flare - firework - indicating players death location (#352)
* Innit

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* geminaj review

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* Add try catch

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* CR

* CR 2

* Add flare damage cancel. Add info to config

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* make it private

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* Resolve ImDMK review

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* CR

* CR

* adjust default configuration to vLucky's review

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathFlareController.java

Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>

---------

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>
Co-authored-by: Rollczi <ndejlich5@gmail.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2026-03-08 18:48:58 +01:00
renovate[bot] c299aed434 dependency: Update Versions.ETERNALCODE_COMMONS to v1.3.4 (#351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 11:19:36 +01:00
renovate[bot]andJakub Kędziora 8dbaf9ae98 dependency: Update actions/upload-artifact action to v7 (#353)
* dependency: Update actions/upload-artifact action to v7

* Remove archive option from upload-artifact step

Remove unnecessary archive option from upload-artifact.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2026-03-07 11:12:59 +01:00
renovate[bot] db4ab96377 dependency: Update dependency org.bstats:bstats-bukkit to v3.2.1 (#350)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 11:12:51 +01:00
renovate[bot] 0dca7e7d42 dependency: Update Gradle to v9.4.0 (#356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 11:12:24 +01:00
renovate[bot] bb080b5ce9 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.3.2 (#354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 10:12:10 +00:00
Martin SulikowskiandRollczi 9f4ae311a7 GH-345 Fix enforcing region knockback for mounted combat players (#345)
* fix: enforce region knockback for mounted combat players

* CR

---------

Co-authored-by: Rollczi <ndejlich5@gmail.com>
2026-03-03 23:28:16 +01:00
Martin Sulikowski 96deb783b7 Update README.md 2026-02-23 23:54:02 +01:00
bee80d22aa GH-313 Migrate issue templates to GitHub Forms and add enhanced contribution documentation.
* Migrate issue templates to GitHub Forms and add enhanced contribution documentation.

* Update .github/CONTRIBUTING.md

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update .github/CONTRIBUTING.md

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update pull request template to use `GH-{NUMBER}` in title format.

* Update .github/CONTRIBUTING.md

* Apply suggestions from code review

* Apply suggestions from code review

* Update label for additional context field

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2026-02-20 15:52:50 +01:00
renovate[bot] 4202e86eaa dependency: Update Versions.ETERNALCODE_COMMONS to v1.3.3 (#349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 15:34:30 +01:00
renovate[bot] e109b38705 dependency: Update junit-framework monorepo to v6.0.3 (#342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 10:50:16 +01:00
renovate[bot] 9bba5bd418 dependency: Update dependency com.github.angeschossen:LandsAPI to v7.24.1 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 10:50:07 +01:00
renovate[bot] 77940ca7b6 dependency: Update dependency org.jetbrains:annotations to v26.1.0 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 10:49:44 +01:00
Michał Wojtas a4cc6d0e8b GH-347 Add lightning effect on player death and settings (#347)
* Add lightning effect on player death and settings

* Add lightning gif to readme

* typo fix

* use public for controller - consistency
2026-02-19 22:54:25 +01:00
Martin Sulikowski 2028c5eafd GH-338 Add option to block specific inventories during combat (#338)
* feat: Add option to block specific inventories during combat

* refactor: Simplify inventory type reference in FightActionBlockerController

* Move to dedicated class.

* refactor: Remove blocking of player's own and creative inventories in FightInventoryController
2026-02-08 20:02:14 +01:00
renovate[bot] 042c5848df dependency: Update dependency me.clip:placeholderapi to v2.12.2 (#340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 20:01:56 +01:00
renovate[bot] c30860070f dependency: Update dependency com.github.retrooper:packetevents-spigot to v2.11.2 (#330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 11:22:15 +01:00
Jakub Kędziora 20c3515082 GH-339 Move renovate.json to .github directory (#339) 2026-02-07 22:00:46 +01:00
Rollczi 07b22a1141 Fix snapshots 2026-02-07 18:38:10 +01:00
renovate[bot] 2fc9bc580b dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.3.10 (#337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 20:53:38 +01:00
renovate[bot] 7730956dbc dependency: Update dependency me.clip:placeholderapi to v2.12.1 (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 20:53:20 +01:00
Rollczi 48ef3e5d0a Simplify snapshots 2026-02-02 20:39:49 +01:00
renovate[bot] abd097401f dependency: Update Gradle to v9.3.1 (#332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 16:22:21 +01:00
Martin Sulikowski 1d0d182cc8 Update Discord invite link in README.md 2026-01-26 00:40:24 +01:00
Martin Sulikowski 60b4e6ee9c bump version to 2.5.0-SNAPSHOT for further development. 2026-01-25 21:15:28 +01:00
Martin Sulikowski b096cb5d6f update dependency references in README 2026-01-25 21:13:08 +01:00
Martin Sulikowski d596202278 Release v2.4.1 2026-01-25 21:07:13 +01:00
Martin Sulikowski b879497840 Add WorldGuard as a soft dependency 2026-01-25 21:06:35 +01:00
renovate[bot] ccad96ae96 dependency: Update gradle/actions action to v5 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 21:39:47 +01:00
Martin Sulikowski 0e782a0cf5 Release 2.5.0-SNAPSHOT 2026-01-23 21:16:40 +01:00
Martin Sulikowski 0979d449c6 Release 2.4.0 2026-01-23 20:37:50 +01:00
renovate[bot] dee65e5f59 dependency: Update actions/upload-artifact action to v6 (#327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:30:16 +01:00
renovate[bot] 01d3491d44 dependency: Update dependency com.github.angeschossen:LandsAPI to v7.23.1 (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:30:00 +01:00
renovate[bot] 9896069577 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.3.1 (#316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:29:53 +01:00
renovate[bot] f682508172 dependency: Update actions/setup-java action to v5 (#326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:29:45 +01:00
renovate[bot] a988abcd17 dependency: Update dependency com.sk89q.worldguard:worldguard-bukkit to v7.0.15 (#307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:26:53 +01:00
renovate[bot] b0fa197e99 dependency: Update Versions.MULTIFICATION to v1.2.4 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:26:42 +01:00
renovate[bot] 5300b5a007 dependency: Update Versions.LITE_COMMANDS to v3.10.9 (#315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:26:07 +01:00
renovate[bot] c1f282832c dependency: Update dependency net.kyori:adventure-text-minimessage to v4.26.1 (#317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:25:23 +01:00
renovate[bot] 972fc52e69 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.3.0 (#318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:25:16 +01:00
renovate[bot] b79ed28edf dependency: Update actions/checkout action to v6 (#320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:25:00 +01:00
renovate[bot] 025f49685c dependency: Update actions/github-script action to v8 (#321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 20:24:53 +01:00
Martin Sulikowski 4d957d2c8a Update README to include bug reporting, feature requests, and donation link 2026-01-23 20:19:54 +01:00
d7a04ef0c2 GH-249 Skip only collidable blocks in the fake wall. Keep plants proper restoring. (#301)
* Update BorderBlockController.java

* Update BorderBlockController.java

* Update BorderBlockController.java

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/border/animation/block/BorderBlockController.java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Follow suggestions

* Code style

* Save WrappedBlockState

* Use inner point for border rendering

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Rollczi <ndejlich5@gmail.com>
2026-01-23 19:54:10 +01:00
Michał Wojtas 0bd9911df3 GH-325 Fix bypass permission and excludeAdminsFromCombat interaction (#325)
* Fix information about bypass permission

* Changes requested after talking to vLucky
2026-01-23 00:01:09 +01:00
Martin Sulikowski 3062378c66 Improve -SNAPSHOT version update checker. 2026-01-17 02:45:14 +01:00
renovate[bot] 0799081813 dependency: Update dependency me.clip:placeholderapi to v2.11.7 (#300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 02:21:24 +01:00
renovate[bot] 16a2df1a08 dependency: Update junit-framework monorepo to v6.0.2 (#299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 02:21:20 +01:00
renovate[bot] cd3681f8f6 dependency: Update Gradle to v9.3.0 (#298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 02:21:14 +01:00
renovate[bot] 885f514e17 dependency: Update dependency com.github.ben-manes.caffeine:caffeine to v3.2.3 (#297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 02:21:10 +01:00
renovate[bot] 5b84881b94 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.2.21 (#294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 02:21:04 +01:00
renovate[bot] e69a42ba0f dependency: Update dependency com.github.retrooper:packetevents-spigot to v2.11.1 (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 02:20:58 +01:00
renovate[bot] 9783d7a025 dependency: Update dependency xyz.jpenilla:run-task to v3.0.2 (#291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 02:20:51 +01:00
Martin Sulikowski 99a93ce249 Bump Hangar publish plugin to version 0.1.4 in build script. 2026-01-17 02:15:29 +01:00
Martin Sulikowski 1c2797225a GH-310 Add Hangar publishing workflow and configure build script for Hangar integration (#312)
* GH-310 Add Hangar publishing workflow and configure build script for Hangar integration

* Expand supported Paper versions in `gradle.properties` to include versions 1.17 through 1.21.11.
2026-01-17 02:09:41 +01:00
renovate[bot] 1af5710ac9 dependency: Update Versions.LITE_COMMANDS to v3.10.8 (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 20:14:35 +00:00
Martin Sulikowski 1938ae409d [ci skip] Update tested Minecraft versions in README to include 1.21.11. 2026-01-16 20:48:57 +01:00
Martin Sulikowski 88fa996202 Update Gradle workflow to use latest upload-artifact action and ensure full git history fetch. Add enhanced error handling for versioning in build script. 2026-01-16 20:13:56 +01:00
Martin Sulikowski e3c4f2c26d Improve versioning logic in build script and set fetch-depth to 0 in Modrinth workflow. 2026-01-16 20:04:52 +01:00
Martin Sulikowski e87060d2b9 Simplify changelog retrieval by removing redundant nullable type qualifier. 2026-01-16 19:24:49 +01:00
Martin Sulikowski dfe70a65d7 Enhance Modrinth workflow to include changelog support 2026-01-16 19:21:20 +01:00
Martin Sulikowski fbade09087 Update Modrinth publishing workflow for release compatibility 2026-01-16 00:53:46 +01:00
Martin Sulikowski 660b8b5519 Bump version to 2.4.0-SNAPSHOT. 2026-01-16 00:53:13 +01:00
Martin Sulikowski 13eb6dead2 Switch MODRINTH_TOKEN retrieval to System.getenv in build script. 2026-01-16 00:32:06 +01:00
Martin Sulikowski eafb4c9318 Simplify version number retrieval in Modrinth configuration. 2026-01-16 00:30:14 +01:00
Martin Sulikowski bfc632d518 GH-309 Add Modrinth publishing workflow and configure Minotaur (#309)
* Add Modrinth publishing workflow and configure Minotaur

* Replace println with logger in build script, retrieve environment variables via providers API.
2026-01-16 00:26:42 +01:00
Michał Wojtas 02d5a2f46d GH-308 Fix issue where master head.ref cannot be accessed (#308) 2026-01-11 19:06:55 +01:00
Michał WojtasandRollczi 9e3f28ee10 GH-305 Update GitHub Actions workflow to set build label for each branch (#305)
* Update GitHub Actions workflow to set build label for release and snapshot

* Fix artifact containing not correct name info

* Adjust space

* Use sha in name as @Jakubk15 suggested

* Fix deprecation warnings

* Fix ugly SHA

* Use gradle instead of gradlew made for gradleWrapper

* run fix

* run fix v2

* Simplify by using github script

* Bump gradle cache

* Remove master / snapshot from name - already branch name is used.

---------

Co-authored-by: Rollczi <ndejlich5@gmail.com>
2026-01-10 20:49:04 +01:00
Michał Wojtas e5fb0d4b32 GH-276 Extract bypass conditions to controller class. (#302)
* Extract bypass conditions to extra class.

Took 2 minutes

* Resolve Gemini review

Took 4 minutes

* Move Controller to controller package and remove empty line

Took 5 minutes

* Use build in Modrinth method to download plugins

Took 23 minutes

* Resolve igoyek's review

Took 3 minutes

* Resolve jakubek's review

Took 15 minutes

* Try #1 resolve imDMK comment

Took 5 minutes
2026-01-07 19:13:11 +01:00
Michał Wojtas 3325a2f8d3 GH-285 Remove border when player is not in combat (#290)
* Remove border when player is not in combat. Add WorldEdit for easier tests

* Add 5 tick delay which reduces 90% of done tests - remove initial solution

* Resolve AI review

* Use our scheduler, add isInCombat check to negate race condition

* Add comment explaining race confition, follow rollczi suggestion
2025-10-13 09:50:51 +02:00
a0bd8494f8 GH-253 Add configurable placeholder settings and new placeholders (#253)
* Add configurable placeholder settings and new placeholders

Introduces PlaceholderSettings to allow customization of placeholder output, specifically for isInCombat_formatted. Adds new placeholders isInCombat and isInCombat_formatted to FightTagPlaceholder, updates BridgeService and PluginConfig to support the new settings, and adjusts command permission annotation placement. Also updates build scripts and dependencies.

* rollback version

* Add papi plugin to runServer

* remove unused import

* follow gemini's suggestion

* Add more comments for configuration, merge time combat placeholders

Took 14 minutes

* change download source to spigot - got 503 on download from github during tests

Took 10 minutes

* follow Rollczi's suggestion -> refactored placeholder registry

* empty string -> null (papi, when receiving null in the code, marks it and sends an empty string)

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/bridge/placeholder/FightTagPlaceholder.java

Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>

* Refactor FightTagPlaceholder and update build script

- Simplified conversion to string in handleIsInCombat.
- Adjusted build script to use Modrinth URLs for plugin downloads and rolled back Minecraft version to 1.21.

---------

Co-authored-by: Michał Wojtas <wojtas.michal90@gmail.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2025-10-13 02:35:07 +02:00
Martin Sulikowski 08cfe7c0c4 Release v2.3.0 2025-10-06 19:50:07 +02:00
Martin Sulikowski ed30d43736 Update readme-banner.png asset. 2025-10-06 19:43:10 +02:00
Martin Sulikowski b6347517b6 Update dependencies and support Minecraft 1.21.9 2025-10-06 19:41:14 +02:00
renovate[bot] 3f729beffe dependency: Update dependency net.kyori:adventure-text-minimessage to v4.25.0 (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 18:51:47 +02:00
Jakub Kędziora 337eb7d765 GH-278 Move updater to eternalcode-commons library (#287)
* Move updater to eternalcode-commons library

* Use gson 2.11.0 for WorldGuard and update gradle.properties to latest standards
2025-10-06 18:49:03 +02:00
renovate[bot] b02231bc0c GH-283 Update junit-framework monorepo to v6 (major)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 18:01:12 +02:00
renovate[bot] ee6f5aecd3 GH-284 Update dependency xyz.jpenilla:run-task to v3.0.1
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 17:57:26 +02:00
renovate[bot] 6eb89607bf GH-286 Update dependency com.github.angeschossen:LandsAPI to v7.17.2
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 17:56:50 +02:00
renovate[bot] 9f33de088b GH-280 Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.2.2
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 17:56:06 +02:00
815b63c7bf GH-188 Add support for folia-based servers. (#275)
* Release v2.3.0-SNAPSHOT

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatSchedulerAdapter.java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Make `CombatSchedulerAdapter` uninstantiable.

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatSchedulerAdapter.java

Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>

* Resolve gemini issues.

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2025-09-29 00:01:58 +02:00
Michał Wojtas 410289a2b8 GH-274 Region priority sort fix (#274) 2025-09-28 23:55:52 +02:00
renovate[bot] e40a37e2ff dependency: Update Gradle to v9.1.0 (#277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 21:38:35 +02:00
renovate[bot] 1bae5c547d dependency: Update dependency com.sk89q.worldguard:worldguard-bukkit to v7.0.14 (#256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:53:52 +02:00
renovate[bot] 986895d5a4 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.2.20 (#246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:47:27 +02:00
renovate[bot] 4776714893 dependency: Update dependency com.github.retrooper:packetevents-spigot to v2.9.5 (#268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:45:06 +02:00
renovate[bot] 6154f26977 dependency: Update dependency eu.okaeri:okaeri-configs-serdes-bukkit to v5.0.13 (#270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:43:58 +02:00
renovate[bot] 2f010ee978 dependency: Update Versions.MULTIFICATION to v1.2.2 (#259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:43:37 +02:00
renovate[bot] 7d5d074811 dependency: Update dependency dev.rollczi:litecommands-bukkit to v3.10.5 (#269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:42:48 +02:00
renovate[bot] 8315e198b3 dependency: Update dependency eu.okaeri:okaeri-configs-serdes-commons to v5.0.13 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:42:13 +02:00
renovate[bot] b0cbdc04c8 dependency: Update dependency org.jetbrains:annotations to v26.0.2-1 (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:42:03 +02:00
renovate[bot] f46f9ca268 dependency: Update dependency xyz.jpenilla:run-task to v3 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 22:41:52 +02:00
renovate[bot] d789d3b47e dependency: Update dependency net.kyori:adventure-text-minimessage to v4.24.0 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 15:43:06 +02:00
renovate[bot] dd7eaf2de8 dependency: Update Versions.ETERNALCODE_COMMONS to v1.3.1 (#261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 15:42:54 +02:00
renovate[bot] a985774272 dependency: Update actions/checkout action to v5 (#262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 15:42:43 +02:00
renovate[bot] fa000b4a3a dependency: Update actions/setup-java action to v5 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 15:42:33 +02:00
renovate[bot] 80edd3b425 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9 (#264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 15:42:24 +02:00
renovate[bot] fc4a6c28a3 dependency: Update dependency gradle to v9 (#265)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 15:42:15 +02:00
vLuckyyy e4eb847b56 Release v2.3.0-SNAPSHOT 2025-08-30 15:23:19 +02:00
renovate[bot] f5884119d6 dependency: Update dependency net.kyori:adventure-platform-bukkit to v4.4.1 (#257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 21:01:59 +02:00
renovate[bot] 0cd9d50086 dependency: Update dependency gradle to v8.14.3 (#241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 21:01:45 +02:00
renovate[bot] 7043d7baff dependency: Update dependency eu.okaeri:okaeri-configs-serdes-bukkit to v5.0.9 (#239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 21:01:28 +02:00
renovate[bot] 26d3f5b1ce dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v8.3.9 (#255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 21:01:13 +02:00
Martin Sulikowski 455b66e136 Use global renovate config. 2025-08-23 20:51:59 +02:00
renovate[bot] 881d9b633a dependency: Update dependency dev.rollczi:litecommands-bukkit to v3.10.4 (#238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:50:44 +02:00
renovate[bot] ac80f1bb0c dependency: Update junit-framework monorepo to v5.13.4 (#243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:49:19 +02:00
renovate[bot] cd36abae37 dependency: Update dependency eu.okaeri:okaeri-configs-serdes-commons to v5.0.9 (#240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:49:03 +02:00
renovate[bot] 817b907170 dependency: Update dependency com.github.retrooper:packetevents-spigot to v2.9.4 (#245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:44:55 +02:00
renovate[bot] b9dfb293e4 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.1.21 (#242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:44:40 +02:00
renovate[bot] 11bfddfc9f dependency: Update dependency com.github.ben-manes.caffeine:caffeine to v3.2.2 (#247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:43:44 +02:00
74d0f487c3 GH-254 Add configuration for disabling fireworks during combat (#254)
* Add FireworkController, move CrystalPvpSettings to configuration package - same as other settings in this project

* Add message sent to the player. Remove unused import

* Add notice service :/

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/CombatSettings.java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/firework/FireworkController.java

Co-authored-by: Igor Michalski <imichalsky00@gmail.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/MessagesSettings.java

Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Igor Michalski <imichalsky00@gmail.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2025-08-22 00:48:46 +02:00
vLuckyyy f4cc51e9a1 Mark as supported for 1.21.8. Fix readme. 2025-07-29 23:23:35 +02:00
Martin SulikowskiandJakubk15 e579ca4a22 Release v2.2.0, Support 1.21.8 (#251)
* Support 1.21.8

* update repo

* Release v2.2.0, remove default sound in combat notification.

---------

Co-authored-by: Jakubk15 <77227023+Jakubk15@users.noreply.github.com>
2025-07-29 23:06:04 +02:00
Michał Wojtas 4c13a61752 GH-250 Update readme with new .gif and new logo (#250)
* Update readme with new .gif and new logo

Took 8 minutes

* Add notice about PacketEvents requirement update banner and resize gif

Took 10 minutes

* Move Crystal pvp section down

Took 1 minute

* Add accessibility changes for gifs

Took 3 minutes
2025-07-27 21:37:01 +02:00
Michał Wojtasandcoderabbitai[bot] dad09ba196 GH-218 Add CrystalPVP expansion (#218)
* Init

* Add anchor support

* Add anchor to api

* Add meta data contants create static methods to retrive uuid. Follow @ImDMK and @coderabbitai reviews

* Use coderabbit suggestion to version compatibility

* Follow coderabbit review

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/crystalpvp/CrystalPvpConstants.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Resolve review. Add documentation comment, add ignored worlds check and event.cancelled check. Fix method name to use `UniqueId` instead of `UUID`

* Remove isCancelled() check -> follow coderabbit <3

* follow coderabbit - add one check for hasDamagerBlockState method declaration check

* Resolve vLuckyyy review

* e -> exception rename

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-22 23:03:30 +02:00
vLuckyyy 690678c1ea Release v2.1.1 2025-07-11 21:18:10 +02:00
renovate[bot] 40cac29eb0 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v8.3.8 (#236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 20:29:41 +02:00
renovate[bot] b9c625cdd6 dependency: Update dependency com.github.ben-manes.caffeine:caffeine to v3.2.1 (#235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 20:26:38 +02:00
renovate[bot] 861aad1852 dependency: Update dependency com.github.angeschossen:LandsAPI to v7.15.20 (#234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 20:26:24 +02:00
vLuckyyy 4c30b7dad6 Migrate renovate config. 2025-07-11 19:18:12 +02:00
Martin Sulikowski 7a152e0624 GH-233 Fix pearl cooldown (#233)
* Fix pearl cooldown

Signed-off-by: vLuckyyy <vluckyyy.biznes@gmail.com>

* Fix cooldown.

---------

Signed-off-by: vLuckyyy <vluckyyy.biznes@gmail.com>
2025-07-11 19:07:20 +02:00
Martin Sulikowski 487fc971bc GH-232 Fix head drop (#232)
* Fix head drop

Signed-off-by: vLuckyyy <vluckyyy.biznes@gmail.com>

* Add minimessages coloring to head drops.

---------

Signed-off-by: vLuckyyy <vluckyyy.biznes@gmail.com>
2025-06-25 00:05:02 +02:00
renovate[bot] ba5fa97e1e dependency: Update dependency net.kyori:adventure-text-minimessage to v4.23.0 (#230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 12:10:21 +02:00
renovate[bot] f2ab289794 dependency: Update minor (#228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 20:51:12 +02:00
Martin SulikowskiandRollczi 241898d62a GH-226 Add Lands support. (#226)
* Add Lands support.

* Fix.

* Fix support.

* Remove useless code. Fix lands implementation. Fix a force knockback system. Fix configuration reloads for border features. Improve default border particle configuration for a better client experience (more fps)

* Fix triggers indexing. Update border on tag event

* Cleanup

* Resolve conflicts

---------

Co-authored-by: Rollczi <ndejlich5@gmail.com>
2025-06-17 20:50:53 +02:00
Martin Sulikowski 9e914d6166 Release v2.1.0 2025-06-14 23:58:15 +02:00
Michał Wojtas f9bc56bf33 GH-227 Sort combat regions by priority which supports overlapping regions in knockback feature (#227)
* Sort combat regions by priority which supports overlapping regions in knockback feature

* Sort combat regions by priority which supports overlapping regions in knockback feature

* Resolve DMK and jakubek review's

* Correct priority
2025-06-10 16:43:10 +02:00
Michał WojtasandRollczi d6bf4e628c GH-225 Add better way to check restricted command - previous idea only checked root commands (#225)
* Add better way to check restricted command - previous idea only checked root commands

Took 2 minutes

* Code review

---------

Co-authored-by: Rollczi <ndejlich5@gmail.com>
2025-05-24 23:23:19 +02:00
renovate[bot] b87e64090a dependency: Update minor (#215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-20 01:03:41 +02:00
7ce6d8bd95 GH-221 Add EventPriority to config for player quit event, add whitelist reasons for kick ex. "Server restart" and untagall command (#221)
* - Added Event priority to config.

* - Some modifications

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/logout/LogoutController.java

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/logout/LogoutController.java

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/logout/LogoutController.java

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/logout/LogoutController.java

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/logout/LogoutController.java

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/logout/LogoutController.java

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>

* - Added whitelist reasons for punishments

* - Added whitelist reasons for punishments

* - Fixed description in CombatSettings

* - Made untag command also executeable via console
- Added untagall command to untag all tagged players at once. useful in cases when you want to do something related to events.

* - Better way of untagging all players.

* - Needs testing - head drops #214
  https://github.com/EternalCodeTeam/EternalCombat/issues/214

* - Added bypass checks for combat

* - Changed the way how bypass is being detected. Using function FightTagController#cannotBeTagged

* - Disabling elytra when get tagged when you are moving. Currently there is a bug where you can still can fly if you are already flying when getting tagged.

* Remove event declaration duplications by DynamicListener and EventManager

---------

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>
Co-authored-by: Rollczi <ndejlich5@gmail.com>
2025-05-20 01:02:30 +02:00
Michał Wojtas 2dc6cbf40c Add option to display milliseconds for last second of combat (#219)
Took 6 minutes
2025-05-11 15:22:21 +02:00
Norbert Dejlich 411e78dda0 GH-213 Fix bossbar configuration serialization (#213) 2025-05-11 14:22:20 +02:00
Michał Wojtas 31d41c900f GH-220 Ignore thorns when causing combat log (#220)
* Ignore thorns damage
* Simply if else statement
2025-04-22 22:03:12 +02:00
Norbert Dejlich a8b48b8804 GH-217 Fix NPE in BorderBlockController (#217) 2025-04-22 20:54:30 +02:00
Martin Sulikowski 312d8e08e6 Release v2.0.1 2025-03-26 21:39:13 +01:00
Martin Sulikowski 7f30d79301 Fix placeholder in "adminPlayerNotInCombat" 2025-03-26 21:37:32 +01:00
Martin Sulikowski 05095857ca Fix PlayerTeleportEvent warn message. 2025-03-26 19:51:17 +01:00
Martin Sulikowski d9f98d3aa2 Fix time formatting for combat message. 2025-03-26 19:40:55 +01:00
Martin Sulikowski bae1b805a7 Release v2.0.1-SNAPSHOT. 2025-03-26 19:36:14 +01:00
Martin Sulikowski 0b119a28c9 Release v2.0.0 2025-03-25 22:09:57 +01:00
Martin Sulikowski a235b799e5 Remove check for updates for packetevents. 2025-03-25 00:21:11 +01:00
Martin Sulikowski 9b8b0a8513 Fix PacketEvents relocation and initialization. 2025-03-25 00:19:33 +01:00
Martin Sulikowski 0da9208323 Update eternalcode-commons. 2025-03-22 22:42:13 +01:00
renovate[bot]andMartin Sulikowski 64e366ccd3 dependency: Update patch (#209)
* dependency: Update patch

* Keep worldguard version.

* Keep multification

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Martin Sulikowski <vLuckyyy.biznes@gmail.com>
2025-03-22 22:41:39 +01:00
renovate[bot] 0540e39af2 dependency: Update all-major (#211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 22:14:58 +01:00
Martin Sulikowski 824313c6ec announcer -> noticeService 2025-03-22 22:13:29 +01:00
Martin Sulikowski 4cd83670a9 GH-186 Make readme great again. (#208)
* Make readme great again.

* Fix table.

* Update README.md

* Update readme banner.
2025-03-22 17:15:18 +01:00
Martin Sulikowski 7f130493f7 Fix java settings. 2025-03-22 02:42:00 +01:00
renovate[bot] 8c5038e3a4 dependency: Update minor (#210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 02:35:07 +01:00
Martin Sulikowski bd4fd131c5 Update renovate settings and remove whitesource. 2025-03-22 02:28:04 +01:00
Martin Sulikowski f338fc4109 Update ShadowJar and gradle. 2025-03-22 02:27:29 +01:00
Martin Sulikowski da555c65e9 GH-190 Remove unused dependencies. 2025-03-22 02:13:50 +01:00
Martin Sulikowski 726f1aa78c GH-189 EternalCombat 2.0. (#189) 2025-03-22 01:08:32 +01:00
Martin Sulikowski 0539e69236 Remove useless import. 2025-03-17 23:32:16 +01:00
Martin Sulikowski d54bff5516 Remove useless NotificationSerializer, rename NotificationAnnouncer to NoticeService 2025-03-17 23:28:18 +01:00
Martin Sulikowski 5c099d2de4 GH-207 Refactor Config (#207)
* Refactor config v2.

* Move to minimessages.
2025-03-17 23:24:56 +01:00
Norbert DejlichandMichał Wojtas 5042241e94 GH-204 Add Border (#204)
* Implement border

* More important changes

* Add blocks updater

* Implement it

* Include combat limitation for border

* Fix knockback

* Fix region resolving

* Remove useless comments

* Cleanup code. Add caches for chunk snapshot

* Code style changes

* Add config for the border and the knockback, cleanup

* Bump actions

* Minor configuration adjustments

Took 30 minutes

* Follow Jakubek review

* Follow DMK review

---------

Co-authored-by: Michał Wojtas <wojtas.michal90@gmail.com>
2025-03-15 22:28:23 +01:00
Michał Wojtas b63e2584ac GH-205 Add on FightUntagEvent allow player flight again, coz its disable every time (#205)
* Add on FightUntagEvent allow player flight again, coz its disable every time

* Update config, update var

* Check Creative gamemode
2025-02-16 18:44:21 +01:00
a6984e2694 GH-202 V2.0/config refactor (#202)
* Refactor config description.

* fix.

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/PluginConfig.java

Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>

* Follow review feedback.

* Refactor default messages.

* Remove gradient from combatNotification.

* Improve some messages.

* Add ASCII tag of EternalCombat in config

---------

Co-authored-by: Michał Wojtas <80779749+CitralFlo@users.noreply.github.com>
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
Co-authored-by: CitralFlo <wojtas.michal90@gmail.com>
2025-02-08 22:26:57 +01:00
Martin Sulikowski c7f93d9b25 Merge branch 'master' into 2.0 2025-02-02 18:50:42 +01:00
Martin Sulikowski bd5a6a4351 Remove checkstyle. 2024-10-24 22:05:56 +02:00
Martin 97fd32a7af Release v1.3.3
Signed-off-by: Martin <vluckyyy.biznes@gmail.com>
2024-10-24 21:30:52 +02:00
165 changed files with 6678 additions and 1707 deletions

No files matched your search

+52 -24
View File
@@ -1,36 +1,64 @@
# Contributing to EternalCombat
# 👋 Contributing to EternalCombat
We welcome all contributions to the EternalCombat project!
First off, thanks for taking the time to contribute! 🎉
We love user contributions and want to make it as easy as possible for you to get involved.
### How to Contribute
## 🛠️ Development Setup
1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Make your changes, and test them thoroughly.
4. Commit your changes and push them to your fork.
5. Submit a pull request to the main repository.
To start contributing, you'll need to set up your environment:
### Code of Conduct
1. **Java 21**: We use Java 21 for development. Ensure you have a JDK 21 installed.
2. **IDE**: We recommend [IntelliJ IDEA](https://www.jetbrains.com/idea/).
3. **Git**: Ensure you have Git installed.
We expect all contributors to follow our [code of conduct](CODE_OF_CONDUCT.md).
### 📥 Getting the Code
### Reporting Issues
```bash
# Fork the repository on GitHub first!
# https://github.com/EternalCodeTeam/EternalCombat/fork
git clone https://github.com/YOUR-USERNAME/EternalCombat.git
cd EternalCombat
```
If you find an issue with the plugin, please report it in
the [Issues tab](https://github.com/EternalCodeTeam/EternalCombat/issues). Please provide as much information as
possible, including the version of Minecraft and the plugin you are using, as well as any error messages or logs.
### 🔨 Building the Project
### Compatibility
Run this command in your terminal:
CombatLog has been tested on versions 1.17.1 - 1.19.3, but probably works on most versions of Minecraft. If you find
compatibility issues, please report them in the Issues tab. CombatLog uses java 11+, so you need to have it installed on
your server.
```bash
./gradlew build
```
### Submitting a Pull Request
## 🤝 How to Contribute
When submitting a pull request, please make sure to follow these guidelines:
1. **Fork & Clone**: Fork the repo to your own account and clone it.
2. **Branch**: Create a new branch for your feature or fix.
```bash
git checkout -b feat/my-awesome-feature
```
3. **Code**: Hack away! 💻
4. **Test**: Run manual test by `runServer` to ensure you haven't broken anything.
```bash
./gradlew runServer
```
5. **Push**: Push your changes to your fork.
6. **Pull Request**: Open a PR against the `master` branch.
- Make sure that your code adheres to the plugin's existing coding style.
- Test your changes thoroughly before submitting the pull request.
- If your pull request is related to an existing issue, please reference the issue in your pull request.
- Thank you for your contributions to the EternalCombat project!
## 📏 Code Style
Make sure that your code adheres to the plugin's existing coding style.
## 🐛 Reporting Bugs
If you find a bug, please use the **Bug Report** issue template.
- Check if the issue already exists.
- Provide a minimal reproduction if possible.
- Include logs and screenshots.
## 💡 Feature Requests
Have an idea? Use the **Feature Request** issue template.
- Explain *why* this feature is useful.
- Describe how it should look or behave.
## 📄 License
By contributing, you agree that your contributions will be licensed under the project's [License](../LICENSE).
-33
View File
@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a bug report to help us improve EternalCombat <3
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Platform (please complete the following information):**
- Java Version: [e.g. JDK Temurin 17]
- Server software, ex. PaperMC 1.19.3 build #1
**Additional notes**
Add any other context about the problem here.
+84
View File
@@ -0,0 +1,84 @@
name: 🐛 Bug Report
description: File a bug report to help us improve EternalCombat
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please ensure that you have searched the existing issues to see if this bug has already been reported.
- type: textarea
id: description
attributes:
label: Bug description
description: A clear and concise description of what the bug is.
placeholder: I was doing X and Y happened...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Please provide a step-by-step guide to reproduce the issue.
placeholder: |
1. Launch the server
2. Tag yourself using the '/combat tag' command
3. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots/Video
description: If applicable, add screenshots or a video to help explain your problem.
- type: dropdown
id: server-software
attributes:
label: Server Software
description: What server software are you using?
options:
- Paper
- Purpur
- Spigot
- Folia
- Other (please specify in logs)
validations:
required: true
- type: input
id: minecraft-version
attributes:
label: Minecraft Version
description: What version of Minecraft are you running? (e.g. 1.21.11)
placeholder: 1.21.11
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: What version of EternalCombat are you running? (Check with /eternalcombat version)
placeholder: v2.4.1
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / Stack Trace
description: Please paste any relevant logs or stack traces here. Use a pastebin service if the log is too long.
render: shell
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Discord Community
url: https://discord.com/invite/FQ7jmGBd6c
about: Please ask questions and discuss ideas in our Discord server before opening a generic issue.
- name: 📖 Documentation
url: https://eternalcode.pl/docs/eternalcombat/introduction
about: Read the documentation to see if your question is already answered.
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for EternalCombat <3
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,38 @@
name: 💡 Feature Request
description: Suggest an idea for EternalCombat
title: "[Feature]: "
labels: ["feature"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a new feature! Please explain your idea in detail.
- type: textarea
id: problem
attributes:
label: Is your feature request related to a problem?
description: Please describe the problem you are trying to solve.
placeholder: I'm always frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
+35 -1
View File
@@ -1 +1,35 @@
**Please describe the changes made by this PR and why they need to be merged:**
---
name: Pull Request
about: Submit a contribution to EternalCombat
title: 'GH-{NUMBER} <title>'
labels: []
assignees: []
---
## 📝 Description
<!--
Please submit your PR using the following title format:
feat: Description of the new feature
fix: Description of the bug fix
chore: Maintenance tasks
docs: Documentation changes
-->
<!-- Describe your changes clearly and concisely. Why is this change needed? What does it accomplish? -->
## 🎯 Type of Change
<!-- Please check the boxes that apply to your changes -->
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] 🔧 Refactoring (no functional changes, just code improvements)
- [ ] 🎨 Style/Cosmetic (formatting, whitespace, visual changes)
- [ ] 📚 Documentation (updates to documentation or README)
- [ ] 🤖 CI/CD/Build (changes to workflows, gradle, etc.)
**Test Environment:**
- **Minecraft Version:** <!-- e.g. 1.20.4 -->
- **Java Version:** <!-- e.g. 21 -->
- **Platform:** <!-- e.g. Windows/Linux/MacOS -->
## 📸 Screenshots (if applicable)
+5
View File
@@ -0,0 +1,5 @@
{
"extends": [
"local>EternalCodeTeam/.github:renovate-config"
]
}
+28 -22
View File
@@ -5,38 +5,44 @@ on:
branches: [ "master" ]
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle
uses: actions/cache@v4.0.2
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Set Variables & Build
uses: actions/github-script@v9
with:
path: ~/.gradle/caches
key: >-
${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*',
'**/gradle-wrapper.properties') }}
restore-keys: '${{ runner.os }}-gradle-'
- name: Make gradlew executable
run: chmod +x gradlew
script: |
const isPR = context.eventName === 'pull_request';
const branch = isPR ? context.payload.pull_request.head.ref.replace(/\//g, '-') : 'master';
const sha = (isPR ? context.payload.pull_request.head.sha : context.sha).substring(0, 7);
const artifactName = `EternalCombat ${branch}+${sha}`;
core.exportVariable('FULL_ARTIFACT_NAME', artifactName);
- name: Build with Gradle
uses: gradle/gradle-build-action@093dfe9d598ec5a42246855d09b49dc76803c005
run: ./gradlew shadowJar
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
arguments: shadowJar
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4.3.6
with:
name: 'Successfully build EternalCombat'
name: ${{ env.FULL_ARTIFACT_NAME }}
path: eternalcombat-plugin/build/libs/*.jar
archive: false
+34
View File
@@ -0,0 +1,34 @@
name: Publish to Hangar
on:
push:
branches:
- master
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
if: (github.event_name == 'push' && !contains(github.event.head_commit.message, '[ci skip]')) || github.event_name == 'release'
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Publish to Hangar
run: ./gradlew :eternalcombat-plugin:publishPluginPublicationToHangar
env:
HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
CHANGELOG: ${{ github.event_name == 'release' && github.event.release.body || github.event.head_commit.message }}
+34
View File
@@ -0,0 +1,34 @@
name: Publish to Modrinth
on:
push:
branches:
- master
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
if: (github.event_name == 'push' && !contains(github.event.head_commit.message, '[ci skip]')) || github.event_name == 'release'
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Publish to Modrinth
run: ./gradlew :eternalcombat-plugin:modrinth
env:
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
CHANGELOG: ${{ github.event_name == 'release' && github.event.release.body || github.event.head_commit.message }}
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
environment: deployment
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
+1
View File
@@ -1,6 +1,7 @@
.classpath
.idea
.gradle
.kotlin
.project
.settings
bin
-14
View File
@@ -1,14 +0,0 @@
{
"scanSettings": {
"baseBranches": []
},
"checkRunSettings": {
"vulnerableCheckRunConclusionLevel": "failure",
"displayMode": "diff",
"useMendCheckNames": true
},
"issueSettings": {
"minSeverityLevel": "LOW",
"issueType": "DEPENDENCY"
}
}
+122 -49
View File
@@ -1,97 +1,170 @@
<div align="center">
![](/assets/readme-banner.png)
![Readme banner](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/readme-banner.png?raw=true)
[![Supports Paper](https://raw.githubusercontent.com/intergrav/devins-badges/v3/assets/cozy/supported/paper_vector.svg)](https://papermc.io)
[![Supports Spigot](https://raw.githubusercontent.com/intergrav/devins-badges/v3/assets/cozy/supported/spigot_vector.svg)](https://spigotmc.org)
[![Available on SpigotMC](https://raw.githubusercontent.com/vLuckyyy/badges/main/available-on-spigotmc.svg)](https://www.spigotmc.org/resources/eternalcombat-%E2%9C%94%EF%B8%8F-enchance-your-combat-system-with-eternalcombat.109056/)
[![Available on Modrinth](https://raw.githubusercontent.com/vLuckyyy/badges/main/avaiable-on-modrinth.svg)](https://modrinth.com/plugin/eternalcombat)
[![Available on Hangar](https://raw.githubusercontent.com/vLuckyyy/badges/main/avaiable-on-hangar.svg)](https://hangar.papermc.io/EternalCodeTeam/eternalcombat)
[![Patreon](https://raw.githubusercontent.com/intergrav/devins-badges/v3/assets/cozy/donate/patreon-plural_vector.svg)](https://www.patreon.com/eternalcode)
[![Website](https://raw.githubusercontent.com/intergrav/devins-badges/v3/assets/cozy/documentation/website_vector.svg)](https://eternalcode.pl/)
[![Discord](https://raw.githubusercontent.com/intergrav/devins-badges/v3/assets/cozy/social/discord-plural_vector.svg)](https://discord.gg/FQ7jmGBd6c)
[![Chat on Discord](https://raw.githubusercontent.com/vLuckyyy/badges/main//chat-with-us-on-discord.svg)](https://discord.com/invite/FQ7jmGBd6c)
[![Read the Docs](https://raw.githubusercontent.com/vLuckyyy/badges/main/read-the-documentation.svg)](https://docs.eternalcode.pl/eternalcombat/introduction)
[![Available on BStats](https://raw.githubusercontent.com/vLuckyyy/badges/main/available-on-bstats.svg)](https://bstats.org/plugin/bukkit/EternalCombat/17803)
[Report Bug](https://github.com/EternalCodeTeam/EternalCombat/issues) • [Request Feature](https://github.com/EternalCodeTeam/EternalCombat/issues) • [Join Discord](https://discord.com/invite/FQ7jmGBd6c)
<br>
<a href="https://ko-fi.com/eternalcodeteam">
<img src="https://github.com/intergrav/devins-badges/blob/v3/assets/cozy/donate/kofi-plural-alt_64h.png?raw=true" height="64" alt="Support us on Ko-fi">
</a>
[![Gradle](https://raw.githubusercontent.com/intergrav/devins-badges/v3/assets/cozy/built-with/gradle_vector.svg)](https://gradle.org/)
[![Java](https://raw.githubusercontent.com/intergrav/devins-badges/v3/assets/cozy/built-with/java17_vector.svg)](https://www.java.com/)
</div>
### Information
EternalCombat has been tested on Minecraft versions 1.17.1 to 1.21.1, but it should work on most other versions. If you
encounter any compatibility issues, please report them in the
[Issues tab](https://github.com/EternalCodeTeam/EternalCombat/issues). The plugin requires Java 17 or later, so please
make sure it is installed on your server.
## EternalCombat requires PacketEvents plugin to work.
You can download it from [here](https://modrinth.com/plugin/packetevents)
## PlaceholderAPI
EternalCombat 2.0 has been tested on Minecraft versions **1.17.1 to 26.1.2**, but it should work seamlessly on most
other versions too.
If you run into any compatibility issues, please let us know in
the [Issues tab](https://github.com/EternalCodeTeam/EternalCombat/issues).
The plugin requires **Java 21 or later**, so
ensure your server is ready.
Ready for action?
Install EternalCombat and dive in now!
EternalCombat supports PlaceholderAPI, which allows you to use placeholders in other plugins that support it.
To use placeholders, follow PlaceholderAPI's [instructions](https://wiki.placeholderapi.com/users/) and use the placeholders provided by EternalCombat.
Provided placeholders:
- `%eternalcombat_opponent%` - Returns the name of the player with whom the player is fighting.
- `%eternalcombat_opponent_health%` - Returns opponent health in format `00.00`
- `%eternalcombat_remaining_seconds%` - Returns the number of seconds remaining until the player is no longer in combat.
- `%eternalcombat_remaining_millis%` - Returns the number of milliseconds remaining until the player is no longer in combat.
### How Does EternalCombat Work?
If the player is not in combat, the placeholders will return an empty string.
If combat was not caused by other player, opponent placeholders will return empty string.
Take your server’s PvP experience to epic heights with **EternalCombat 2.0**! Our robust combat logging system ensures
fair, heart-pounding battles that keep players on their toes. Here’s a rundown of what makes it stand out:
## Building
Build EternalCombat using:
`./gradlew shadowJar`
- **Combat Logging**
No more dodging fights by logging out! Once players are in combat, they’re committed until the showdown ends. Watch it
in action:
<img alt="Combat log anti logout feature" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/combatlog.gif?raw=true" width="420">
- **Customize combat experience**
Add custom effects to players in combat or death. Everything should be configurable and user-friendly:
<img alt="Lightning strikes when players die" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/lightning.gif?raw=true" width="420">
<img alt="Flare indicates players death location" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/flare.gif?raw=true" width="420">
- **Spawn Protection (Configurable)**
Stop players from fleeing to safety! Block access to spawn or safe zones during combat – tweak it to fit your server’s
rules. See how it works:
<img alt="Border around protected region" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/border.gif?raw=true" width="420">
- **Crystal PvP support**
Engage in intense Crystal PvP battles without worrying about players logging out mid-fight! EternalCombat keeps
everyone in the game until the last anchor hit. Check it out:
<img alt="Crystal PvP showcase" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/crystals.gif?raw=true" width="420">
- **Fully Customizable Combat**
Tailor the combat experience to your liking with a ton of options! From disabling elytra to setting drop rates for
defeated players, you’re in control. Here’s what you can tweak:
| Feature | Description |
|----------------------|-----------------------------------------------------------------|
| Elytra & Inventory | Disable elytra or inventory access during combat. |
| Commands | Whitelist or blacklist specific commands in combat. |
| Damage & Projectiles | Customize damage causes and projectile tagging settings. |
| Ender Pearls | Add cooldowns to pearl usage in fights. |
| Block Placement | Enable or disable block placement during combat. |
| Drop Rates | Set a percentage of items dropped by defeated players. |
| Temporary Effects | Apply special effects to players in combat for added intensity. |
| And More! | Dive into the config for even more fine-tuning options! |
Curious for more? Check out our quick and exciting YouTube presentation [here](https://youtu.be/5pELO5B0Hhk) to see
EternalCombat in full swing and learn why it’s a game-changer for your server!
### Permissions for EternalCombat
- `eternalcombat.status` - Allows to check if the player is in combat `/combatlog status <player>`
- `eternalcombat.tag` - Allows to create fights between two
players `/combatlog tag <first_player> <optional_second_player>`
- `eternalcombat.untag` - Allows to remove player from fight `/combatlog untag <player>`
- `eternalcombat.reload` - Allows to reload plugin `/combatlog reload`
- `eternalcombat.receiveupdates` - Allows you to receive notifications about the new version of the plugin when
attaching
Control who can use EternalCombat’s powerful features with these permissions:
| Permission | Description |
|--------------------------------|-------------------------------------------------------------------------------------------------------|
| `eternalcombat.status` | Check a player’s combat status with `/combatlog status <player>`. |
| `eternalcombat.tag` | Start a fight between players using `/combatlog tag <player> [player2]`. |
| `eternalcombat.untag` | Remove a player from combat with `/combatlog untag <player>`. |
| `eternalcombat.untagall` | Remove all players from combat with `/combatlog untagall`. |
| `eternalcombat.stats` | View combat statistics with `/combatlog stats`. |
| `eternalcombat.reload` | Reload the plugin with `/combatlog reload`. |
| `eternalcombat.receiveupdates` | Receive notifications about new plugin versions on join. |
| `eternalcombat.bypass` | When exclude `excludeAdminsFromCombat` is set to `true` users with this permission will not be tagged |
## PlaceholderAPI
EternalCombat 2.0 fully supports **PlaceholderAPI**, letting you integrate placeholders into other compatible plugins.
Follow the [PlaceholderAPI instructions](https://wiki.placeholderapi.com/users/) to get started.
Here are the available
placeholders:
| Placeholder | Description |
|-------------------------------------|---------------------------------------------------------------|
| `%eternalcombat_opponent%` | Returns the name of the player you’re fighting. |
| `%eternalcombat_opponent_health%` | Returns the opponent’s health in `00.00` format. |
| `%eternalcombat_remaining_seconds%` | Returns seconds remaining until the player exits combat. |
| `%eternalcombat_remaining_millis%` | Returns milliseconds remaining until the player exits combat. |
If a player isn’t in combat, placeholders return an empty string.
If combat wasn’t triggered by another player,
opponent-related placeholders will also return empty.
### Developer API
#### 1. Add repository:
with Gradle:
With Gradle:
```kts
maven {
url = uri("https://repo.eternalcode.pl/releases")
}
```
with Maven:
With Maven:
```xml
<repository>
<id>eternalcode-reposilite-releases</id>
<name>EternalCode Repository</name>
<url>https://repo.eternalcode.pl/releases</url>
<id>eternalcode-reposilite-releases</id>
<name>EternalCode Repository</name>
<url>https://repo.eternalcode.pl/releases</url>
</repository>
```
#### 2. Add dependency:
with Gradle:
With Gradle:
```kts
compileOnly("com.eternalcode:eternalcombat-api:1.3.2")
compileOnly("com.eternalcode:eternalcombat-api:2.5.2-SNAPSHOT")
```
with Maven:
With Maven:
```xml
<dependency>
<groupId>com.eternalcode</groupId>
<artifactId>eternalcombat-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope>
<groupId>com.eternalcode</groupId>
<artifactId>eternalcombat-api</artifactId>
<version>2.5.2-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
```
### Contributing
We welcome all contributions to the EternalCombat project! Please check out [contributing](.github/CONTRIBUTING.md) for
more information on how to contribute and our [code of conduct](./.github/CODE_OF_CONDUCT.md)
We’d love your help to make EternalCombat even better!
Check out our [contributing guide](https://github.com/EternalCodeTeam/EternalCombat/blob/master/.github/CONTRIBUTING.md) for
details on how to get involved and our [code of conduct](https://github.com/EternalCodeTeam/EternalCombat/blob/master/.github/CODE_OF_CONDUCT.md).
### Reporting Issues
If you find an issue with the plugin, please report it in
the [Issues tab](https://github.com/EternalCodeTeam/EternalCombat/issues). Please provide as much information as
possible, including the version of Minecraft and the plugin you are using, as well as any error messages or logs
Found a bug?
Report it in the [Issues tab](https://github.com/eternalcodeteam/eternalcombat/issues).
Please include as much detail as possible, like your Minecraft and plugin
versions, along with any error messages or logs.
Ready to transform your server’s combat experience?
Download EternalCombat 2.0 now and let the battles begin!
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 98 KiB

+5 -3
View File
@@ -8,10 +8,12 @@ repositories {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.25")
implementation("io.github.goooler.shadow:shadow-gradle-plugin:8.1.8")
implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.6.1")
implementation("com.modrinth.minotaur:com.modrinth.minotaur.gradle.plugin:2.9.0")
implementation("io.papermc.hangar-publish-plugin:io.papermc.hangar-publish-plugin.gradle.plugin:0.1.4")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.4.10")
implementation("net.minecrell:plugin-yml:0.6.0")
implementation("xyz.jpenilla:run-task:2.3.0")
implementation("xyz.jpenilla:run-task:3.0.2")
}
sourceSets {
@@ -0,0 +1,39 @@
import org.gradle.api.Project
import org.gradle.api.provider.Provider
internal val Project.isRelease: Boolean
get() = providers.environmentVariable("GITHUB_EVENT_NAME").orNull == "release"
internal val Project.releaseChannel: String
get() = if (isRelease) "Release" else "Snapshot"
internal val Project.changelogText: Provider<String>
get() = providers.environmentVariable("CHANGELOG")
.orElse(providers.exec { commandLine("git", "log", "-1", "--format=%B") }.standardOutput.asText)
internal val Project.paperVersions: List<String>
get() = property("paperVersion").toString()
.split(",")
.map { paperVersion -> paperVersion.trim() }
internal fun Project.configureSnapshotVersionFromGit() {
val isGithubWorkflow = providers.environmentVariable("GITHUB_RUN_NUMBER").isPresent
if (!isGithubWorkflow || isRelease) {
return
}
if ("-SNAPSHOT+" in version.toString()) {
return
}
val describeParts = providers.exec { commandLine("git", "describe", "--tags", "--long") }
.standardOutput.asText.get()
.trim()
.split("-")
val offset = if (describeParts.size >= 3) describeParts[describeParts.size - 2] else "0"
val baseVersion = version.toString().substringBefore("-SNAPSHOT")
version = "$baseVersion-SNAPSHOT+$offset"
}
+22 -21
View File
@@ -1,34 +1,35 @@
object Versions {
const val SPIGOT_API = "1.17.1-R0.1-SNAPSHOT"
// Keep this version for best compatibility.
const val PAPER_API = "1.19.3-R0.1-SNAPSHOT"
const val JUNIT_JUPITER_API = "5.10.3"
const val JUNIT_JUPITER_PARAMS = "5.10.3"
const val JUNIT_JUPITER_ENGINE = "5.10.3"
const val JUNIT_JUPITER_API = "6.1.3"
const val JUNIT_JUPITER_PARAMS = "6.1.3"
const val JUNIT_JUPITER_ENGINE = "6.1.3"
const val JETBRAINS_ANNOTATIONS = "24.1.0"
const val JETBRAINS_ANNOTATIONS = "26.1.0"
const val ETERNALCODE_COMMONS = "1.1.3"
const val MULTIFICATION = "1.1.4"
const val ETERNALCODE_COMMONS = "1.4.1"
const val MULTIFICATION = "1.2.5-SNAPSHOT"
const val PACKETS_EVENTS = "2.13.0"
const val ADVENTURE_PLATFORM_BUKKIT = "4.3.4"
const val ADVENTURE_API = "4.17.0"
const val LITE_COMMANDS = "3.11.0"
const val OKAERI_CONFIGS_SERDES_COMMONS = "5.0.13"
const val OKAERI_CONFIGS_SERDES_BUKKIT = "5.0.13"
const val LITE_COMMANDS = "3.7.0"
const val OKAERI_CONFIGS_YAML_BUKKIT = "5.0.3"
const val OKAERI_CONFIGS_SERDES_COMMONS = "5.0.3"
const val OKAERI_CONFIGS_SERDES_BUKKIT = "5.0.3"
const val XSERIES = "13.8.0"
const val PANDA_UTILITIES = "0.5.2-alpha"
const val GIT_CHECK = "1.0.0"
const val APACHE_COMMONS = "2.16.1"
const val CAFFEINE = "3.2.4"
const val CAFFEINE = "3.1.8"
const val B_STATS_BUKKIT = "3.2.1"
const val WORLD_GUARD_BUKKIT = "7.0.17"
const val B_STATS_BUKKIT = "3.0.2"
const val WORLD_GUARD_BUKKIT = "7.0.9"
const val PLACEHOLDER_API = "2.11.6"
const val PLACEHOLDER_API = "2.12.3"
const val LANDS_API = "7.25.4"
const val WORLDEDIT = "3ISh7ADm" //cannot use numeric version bc of duplicated version on modrinth
const val PACKETEVENTS = "2.11.1"
const val WORLDGUARD = "7.0.15-beta-01"
const val LUCKPERMS = "5.5.17"
}
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}")
testImplementation("io.papermc.paper:paper-api:${Versions.PAPER_API}")
testImplementation("org.junit.jupiter:junit-jupiter-api:${Versions.JUNIT_JUPITER_API}")
testImplementation("org.junit.jupiter:junit-jupiter-params:${Versions.JUNIT_JUPITER_PARAMS}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Versions.JUNIT_JUPITER_ENGINE}")
@@ -1,40 +1,14 @@
plugins {
`java-library`
checkstyle
}
group = "com.eternalcode"
version = "2.0.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
checkstyle {
toolVersion = "10.17.0"
configFile = file("${rootDir}/config/checkstyle/checkstyle.xml")
maxErrors = 0
maxWarnings = 0
}
// https://github.com/JabRef/jabref/pull/10812/files#diff-49a96e7eea8a94af862798a45174e6ac43eb4f8b4bd40759b5da63ba31ec3ef7R267
configurations.named("checkstyle") {
resolutionStrategy {
capabilitiesResolution {
withCapability("com.google.collections:google-collections") {
select("com.google.guava:guava:33.2.1-jre")
}
}
}
}
version = "2.5.2-SNAPSHOT"
tasks.compileJava {
options.compilerArgs = listOf("-Xlint:deprecation", "-parameters")
options.encoding = "UTF-8"
options.release = 17
options.release = 21
}
java {
@@ -0,0 +1,28 @@
import io.papermc.hangarpublishplugin.model.Platforms
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
id("io.papermc.hangar-publish-plugin")
}
configureSnapshotVersionFromGit()
pluginManager.withPlugin("com.gradleup.shadow") {
val shadowJarTask = tasks.named<ShadowJar>("shadowJar")
hangarPublish {
publications.register("plugin") {
version.set(project.version.toString())
channel.set(project.releaseChannel)
changelog.set(project.changelogText)
id.set("eternalcombat")
apiKey.set(providers.environmentVariable("HANGAR_API_TOKEN"))
platforms {
register(Platforms.PAPER) {
jar.set(shadowJarTask.flatMap { shadowJar -> shadowJar.archiveFile })
platformVersions.set(project.paperVersions)
}
}
}
}
}
@@ -0,0 +1,23 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
id("com.modrinth.minotaur")
}
configureSnapshotVersionFromGit()
pluginManager.withPlugin("com.gradleup.shadow") {
val shadowJarTask = tasks.named<ShadowJar>("shadowJar")
modrinth {
token.set(providers.environmentVariable("MODRINTH_TOKEN"))
projectId.set("eternalcombat")
versionNumber.set(project.version.toString())
versionType.set(if (project.isRelease) "release" else "beta")
uploadFile.set(shadowJarTask)
gameVersions.addAll(project.paperVersions)
loaders.addAll(listOf("paper", "folia", "purpur"))
changelog.set(project.changelogText)
syncBodyFrom.set(rootProject.file("README.md").readText())
}
}
@@ -4,7 +4,7 @@ plugins {
}
group = "com.eternalcode"
version = "2.0.0-SNAPSHOT"
version = "2.5.2-SNAPSHOT"
java {
withSourcesJar()
@@ -3,13 +3,17 @@ plugins {
}
repositories {
mavenLocal()
mavenCentral()
maven { url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") }
maven { url = uri("https://papermc.io/repo/repository/maven-public/") }
maven { url = uri("https://repo.eternalcode.pl/releases") }
maven { url = uri("https://storehouse.okaeri.eu/repository/maven-public/") }
maven { url = uri("https://repo.panda-lang.org/releases") }
maven { url = uri("https://maven.enginehub.org/repo/") }
maven { url = uri("https://repo.extendedclip.com/content/repositories/placeholderapi/")}
maven("https://repo.eternalcode.pl/releases")
maven("https://repo.eternalcode.pl/snapshots")
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://storehouse.okaeri.eu/repository/maven-public/")
maven("https://repo.panda-lang.org/releases")
maven("https://maven.enginehub.org/repo/")
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
maven("https://repo.codemc.io/repository/maven-releases/")
maven("https://jitpack.io/")
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
@@ -0,0 +1,22 @@
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.jvm.toolchain.JavaToolchainService
plugins {
id("xyz.jpenilla.run-paper")
}
tasks {
val javaToolchains = extensions.getByType<JavaToolchainService>()
runServer {
javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(25))
})
minecraftVersion("26.1.2")
downloadPlugins.modrinth("FastAsyncWorldEdit", "2.15.2")
downloadPlugins.modrinth("PacketEvents", "2.12.2+spigot")
downloadPlugins.modrinth("WorldGuard", "7.0.17")
downloadPlugins.modrinth("LuckPerms", "v5.5.53-bukkit")
}
}
+2 -2
View File
@@ -6,7 +6,7 @@ plugins {
}
dependencies {
// Spigot api
compileOnlyApi("org.spigotmc:spigot-api:${Versions.SPIGOT_API}")
// Paper api
compileOnlyApi("io.papermc.paper:paper-api:${Versions.PAPER_API}")
api("org.jetbrains:annotations:${Versions.JETBRAINS_ANNOTATIONS}")
}
@@ -4,7 +4,7 @@ import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.drop.DropService;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.pearl.FightPearlService;
import com.eternalcode.combat.fight.pearl.PearlService;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
@@ -14,7 +14,7 @@ public interface EternalCombatApi {
RegionProvider getRegionProvider();
FightPearlService getFightPearlService();
PearlService getFightPearlService();
FightTagOutService getFightTagOutService();
@@ -2,6 +2,9 @@ package com.eternalcode.combat.fight.event;
public enum CancelTagReason {
TAGOUT
TAGOUT,
PERMISSION_BYPASS,
ADMIN,
CREATIVE_MODE,
}
@@ -18,6 +18,27 @@ public enum CauseOfTag {
*/
COMMAND,
/**
* Crystal or anchor explosion caused the tag.
*/
CRYSTAL,
/**
* Trident usage extending combat tag.
*/
TRIDENT,
/**
* Ender pearl usage extending combat tag.
*/
ENDER_PEARL,
/**
* The player's combat tag was kept active because they were below the void combat height.
*/
VOID,
/**
* A custom cause, typically defined by external plugins or systems, applied the combat tag.
*/
@@ -1,16 +1,16 @@
package com.eternalcode.combat.fight.pearl;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
import org.bukkit.entity.Player;
public interface FightPearlService {
public interface PearlService {
Instant getDelay(UUID uuid);
boolean shouldCancelEvent(UUID playerId);
Duration getRemainingDelay(UUID uuid);
boolean hasDelay(UUID uuid);
void markDelay(UUID uuid);
void handleDelay(Player uuid);
}
@@ -0,0 +1,31 @@
package com.eternalcode.combat.fight.trident;
import java.time.Duration;
import java.util.UUID;
public interface TridentService {
/**
* returns remaining delay for the player to use trident again
* @param uuid unique id of the player
* @return remaining duration left to use trident again by the player or zero
*/
Duration getRemainingDelay(UUID uuid);
/**
* checks if player still has delay left to use trident
* @param uuid unique id of the player
* @return true if user still has cooldown left to use trident
*/
boolean hasDelay(UUID uuid);
/**
* marks start of the delay for the user
* @param uuid unique id of the player
*/
void markDelay(UUID uuid);
void removeDelay(UUID playerId);
}
@@ -0,0 +1,6 @@
package com.eternalcode.combat.region;
import org.bukkit.World;
public record Point(World world, double x, double z) {
}
@@ -4,6 +4,22 @@ import org.bukkit.Location;
public interface Region {
Location getCenter();
Point getCenter();
Location getMin();
Location getMax();
default boolean contains(Location location) {
return this.contains(location.getX(), location.getY(), location.getZ());
}
default boolean contains(double x, double y, double z) {
Location min = this.getMin();
Location max = this.getMax();
return x >= min.getX() && x < max.getX()
&& y >= min.getY() && y < max.getY()
&& z >= min.getZ() && z < max.getZ();
}
}
@@ -1,7 +1,9 @@
package com.eternalcode.combat.region;
import java.util.Collection;
import java.util.Optional;
import org.bukkit.Location;
import org.bukkit.World;
public interface RegionProvider {
@@ -11,4 +13,6 @@ public interface RegionProvider {
return this.getRegion(location).isPresent();
}
Collection<Region> getRegions(World world);
}
+74 -40
View File
@@ -1,40 +1,45 @@
import net.minecrell.pluginyml.paper.PaperPluginDescription
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
plugins {
`eternalcombat-java`
`eternalcombat-repositories`
`eternalcombat-publish-hangar`
`eternalcombat-publish-modrinth`
`eternalcombat-runserver`
id("net.minecrell.plugin-yml.bukkit")
id("io.github.goooler.shadow")
id("xyz.jpenilla.run-paper")
id("de.eldoria.plugin-yml.paper") version "0.9.0"
id("com.gradleup.shadow")
}
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "com.google.code.gson" && requested.name == "gson") {
useVersion("2.11.0")
because("WorldGuard requires strictly gson 2.11.0")
}
}
}
}
dependencies {
implementation(project(":eternalcombat-api"))
// kyori
implementation("net.kyori:adventure-platform-bukkit:${Versions.ADVENTURE_PLATFORM_BUKKIT}")
implementation("net.kyori:adventure-text-minimessage:${Versions.ADVENTURE_API}")
implementation("net.kyori:adventure-api") {
version {
strictly(Versions.ADVENTURE_API)
}
}
// Paper
compileOnly("io.papermc.paper:paper-api:${Versions.PAPER_API}")
// litecommands
implementation("dev.rollczi:litecommands-bukkit:${Versions.LITE_COMMANDS}")
implementation("dev.rollczi:litecommands-folia:${Versions.LITE_COMMANDS}")
// Okaeri configs
implementation("eu.okaeri:okaeri-configs-yaml-bukkit:${Versions.OKAERI_CONFIGS_YAML_BUKKIT}")
implementation("eu.okaeri:okaeri-configs-serdes-commons:${Versions.OKAERI_CONFIGS_SERDES_COMMONS}")
implementation("eu.okaeri:okaeri-configs-serdes-bukkit:${Versions.OKAERI_CONFIGS_SERDES_BUKKIT}")
// Panda utilities
implementation("org.panda-lang:panda-utilities:${Versions.PANDA_UTILITIES}")
// GitCheck
implementation("com.eternalcode:gitcheck:${Versions.GIT_CHECK}")
// commons
implementation("commons-io:commons-io:${Versions.APACHE_COMMONS}")
// XSeries
implementation("com.github.cryptomorin:XSeries:${Versions.XSERIES}")
// bstats
implementation("org.bstats:bstats-bukkit:${Versions.B_STATS_BUKKIT}")
@@ -42,8 +47,11 @@ dependencies {
// caffeine
implementation("com.github.ben-manes.caffeine:caffeine:${Versions.CAFFEINE}")
implementation("com.eternalcode:eternalcode-commons-bukkit:${Versions.ETERNALCODE_COMMONS}")
implementation("com.eternalcode:eternalcode-commons-adventure:${Versions.ETERNALCODE_COMMONS}")
implementation("com.eternalcode:eternalcode-commons-bukkit:${Versions.ETERNALCODE_COMMONS}")
implementation("com.eternalcode:eternalcode-commons-shared:${Versions.ETERNALCODE_COMMONS}")
implementation("com.eternalcode:eternalcode-commons-folia:${Versions.ETERNALCODE_COMMONS}")
implementation("com.eternalcode:eternalcode-commons-updater:${Versions.ETERNALCODE_COMMONS}")
// worldguard
compileOnly("com.sk89q.worldguard:worldguard-bukkit:${Versions.WORLD_GUARD_BUKKIT}")
@@ -51,58 +59,84 @@ dependencies {
// PlaceholderAPI
compileOnly("me.clip:placeholderapi:${Versions.PLACEHOLDER_API}")
// Lands
compileOnly("com.github.angeschossen:LandsAPI:${Versions.LANDS_API}")
// Multification
implementation("com.eternalcode:multification-bukkit:${Versions.MULTIFICATION}")
implementation("com.eternalcode:multification-paper:${Versions.MULTIFICATION}")
implementation("com.eternalcode:multification-okaeri:${Versions.MULTIFICATION}")
compileOnly("com.github.retrooper:packetevents-spigot:${Versions.PACKETS_EVENTS}")
}
bukkit {
paper {
main = "com.eternalcode.combat.CombatPlugin"
author = "EternalCodeTeam"
apiVersion = "1.13"
authors = listOf("EternalCodeTeam")
apiVersion = "1.19"
prefix = "EternalCombat"
name = "EternalCombat"
softDepend = listOf("WorldGuard", "PlaceholderAPI")
generateLibrariesJson = true
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
version = "${project.version}"
foliaSupported = true
serverDependencies {
register("packetevents") {
required = true
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("Lands") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("WorldGuard") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("PlaceholderAPI") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
}
}
tasks {
runServer {
minecraftVersion("1.21.1")
named("generatePaperPluginDescription") {
notCompatibleWithConfigurationCache("The plugin-yml paper generator reads Task.project during execution.")
}
}
tasks.shadowJar {
archiveFileName.set("EternalCombat v${project.version}.jar")
dependsOn("checkstyleMain")
dependsOn("checkstyleTest")
dependsOn("test")
exclude(
"org/intellij/lang/annotations/**",
"org/jetbrains/annotations/**",
"net/kyori/**",
"META-INF/**",
"kotlin/**",
"javax/**"
"javax/**",
"org/checkerframework/**",
"com/google/errorprone/**",
"com/google/gson/**"
)
val prefix = "com.eternalcode.combat.libs"
listOf(
"panda.std",
"panda.utilities",
"org.panda-lang",
"eu.okaeri",
"net.kyori",
"org.bstats",
"org.yaml",
"dev.rollczi.litecommands",
"com.eternalcode.gitcheck",
"org.json.simple",
"org.apache.commons",
"javassist",
"com.github.benmanes.caffeine",
"com.eternalcode.commons",
"com.eternalcode.multification"
"com.eternalcode.multification",
"com.github.cryptomorin",
"com.cryptomorin",
).forEach { pack ->
relocate(pack, "$prefix.$pack")
}
@@ -1,56 +1,83 @@
package com.eternalcode.combat;
import com.eternalcode.combat.border.BorderService;
import com.eternalcode.combat.border.BorderServiceImpl;
import com.eternalcode.combat.border.BorderTriggerController;
import com.eternalcode.combat.border.animation.block.BorderBlockController;
import com.eternalcode.combat.border.animation.particle.ParticleController;
import com.eternalcode.combat.bridge.BridgeService;
import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.drop.DropService;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
import com.eternalcode.combat.fight.pearl.FightPearlService;
import com.eternalcode.combat.handler.InvalidUsageHandlerImpl;
import com.eternalcode.combat.handler.MissingPermissionHandlerImpl;
import com.eternalcode.combat.config.ConfigService;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.drop.DropController;
import com.eternalcode.combat.fight.drop.DropKeepInventoryServiceImpl;
import com.eternalcode.combat.fight.drop.DropServiceImpl;
import com.eternalcode.combat.fight.drop.impl.PercentDropModifier;
import com.eternalcode.combat.fight.drop.impl.PlayersHealthDropModifier;
import com.eternalcode.combat.crystalpvp.EndCrystalListener;
import com.eternalcode.combat.crystalpvp.RespawnAnchorListener;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.FightManagerImpl;
import com.eternalcode.combat.fight.FightTagCommand;
import com.eternalcode.combat.fight.controller.FightActionBlockerController;
import com.eternalcode.combat.fight.FightTask;
import com.eternalcode.combat.fight.blocker.CommandsBlocker;
import com.eternalcode.combat.fight.blocker.ElytraBlocker;
import com.eternalcode.combat.fight.blocker.ElytraEquipBlocker;
import com.eternalcode.combat.fight.blocker.FlyingBlocker;
import com.eternalcode.combat.fight.controller.FightBypassAdminController;
import com.eternalcode.combat.fight.controller.FightBypassCreativeController;
import com.eternalcode.combat.fight.controller.FightBypassPermissionController;
import com.eternalcode.combat.fight.blocker.InventoryContainersBlocker;
import com.eternalcode.combat.fight.controller.FightMessageController;
import com.eternalcode.combat.fight.controller.FightTagController;
import com.eternalcode.combat.fight.controller.FightUnTagController;
import com.eternalcode.combat.fight.death.DeathFlareController;
import com.eternalcode.combat.fight.death.DeathLightningController;
import com.eternalcode.combat.fight.drop.DropController;
import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
import com.eternalcode.combat.fight.drop.DropKeepInventoryServiceImpl;
import com.eternalcode.combat.fight.drop.DropService;
import com.eternalcode.combat.fight.drop.DropServiceImpl;
import com.eternalcode.combat.fight.drop.impl.PercentDropModifier;
import com.eternalcode.combat.fight.drop.impl.PlayersHealthDropModifier;
import com.eternalcode.combat.fight.blocker.PlaceBlockBlocker;
import com.eternalcode.combat.fight.blocker.SignEditingBlocker;
import com.eternalcode.combat.fight.effect.FightEffectController;
import com.eternalcode.combat.event.EventCaller;
import com.eternalcode.combat.fight.FightManagerImpl;
import com.eternalcode.combat.fight.FightTask;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.effect.FightEffectServiceImpl;
import com.eternalcode.combat.fight.firework.FireworkController;
import com.eternalcode.combat.fight.knockback.KnockbackMountController;
import com.eternalcode.combat.fight.knockback.KnockbackRegionController;
import com.eternalcode.combat.fight.knockback.KnockbackService;
import com.eternalcode.combat.fight.logout.LogoutController;
import com.eternalcode.combat.fight.logout.LogoutService;
import com.eternalcode.combat.fight.pearl.FightPearlController;
import com.eternalcode.combat.fight.pearl.FightPearlServiceImpl;
import com.eternalcode.combat.fight.tagout.FightTagOutController;
import com.eternalcode.combat.fight.tagout.FightTagOutServiceImpl;
import com.eternalcode.combat.fight.pearl.PearlController;
import com.eternalcode.combat.fight.pearl.PearlService;
import com.eternalcode.combat.fight.pearl.PearlServiceImpl;
import com.eternalcode.combat.fight.tagout.FightTagOutCommand;
import com.eternalcode.combat.notification.NotificationAnnouncer;
import com.eternalcode.combat.region.RegionController;
import com.eternalcode.combat.fight.tagout.FightTagOutController;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
import com.eternalcode.combat.fight.tagout.FightTagOutServiceImpl;
import com.eternalcode.combat.fight.trident.TridentController;
import com.eternalcode.combat.fight.trident.TridentService;
import com.eternalcode.combat.fight.trident.TridentServiceImpl;
import com.eternalcode.combat.handler.InvalidUsageHandlerImpl;
import com.eternalcode.combat.handler.MissingPermissionHandlerImpl;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.updater.UpdaterNotificationController;
import com.eternalcode.combat.updater.UpdaterService;
import com.eternalcode.commons.adventure.AdventureLegacyColorPostProcessor;
import com.eternalcode.commons.adventure.AdventureLegacyColorPreProcessor;
import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler;
import com.eternalcode.multification.notice.Notice;
import com.google.common.base.Stopwatch;
import dev.rollczi.litecommands.LiteCommands;
import dev.rollczi.litecommands.bukkit.LiteBukkitFactory;
import dev.rollczi.litecommands.bukkit.LiteBukkitMessages;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import dev.rollczi.litecommands.folia.FoliaExtension;
import java.time.Duration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bstats.bukkit.Metrics;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
@@ -63,21 +90,19 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
private static final int BSTATS_METRICS_ID = 17803;
private FightManager fightManager;
private FightPearlService fightPearlService;
private PearlService pearlService;
private TridentService tridentService;
private FightTagOutService fightTagOutService;
private FightEffectService fightEffectService;
private LogoutService logoutService;
private DropService dropService;
private DropKeepInventoryService dropKeepInventoryService;
private RegionProvider regionProvider;
private PluginConfig pluginConfig;
private AudienceProvider audienceProvider;
private LiteCommands<CommandSender> liteCommands;
private boolean apiInitialized;
@Override
public void onEnable() {
@@ -88,79 +113,123 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
ConfigService configService = new ConfigService();
EventCaller eventCaller = new EventCaller(server);
EventManager eventManager = new EventManager(this);
PluginConfig pluginConfig = configService.create(PluginConfig.class, new File(dataFolder, "config.yml"));
this.pluginConfig = configService.create(PluginConfig.class, new File(dataFolder, "config.yml"));
MinecraftScheduler scheduler = CombatSchedulerAdapter.getAdaptiveScheduler(this);
this.fightManager = new FightManagerImpl(eventCaller);
this.fightPearlService = new FightPearlServiceImpl(this.pluginConfig.pearl);
this.fightManager = new FightManagerImpl(eventManager);
this.pearlService = new PearlServiceImpl(this.fightManager, pluginConfig, scheduler);
this.tridentService = new TridentServiceImpl(pluginConfig);
this.fightTagOutService = new FightTagOutServiceImpl();
this.fightEffectService = new FightEffectServiceImpl();
this.logoutService = new LogoutService();
LogoutService logoutService = new LogoutService();
this.dropService = new DropServiceImpl();
this.dropKeepInventoryService = new DropKeepInventoryServiceImpl();
UpdaterService updaterService = new UpdaterService(this.getDescription());
this.audienceProvider = BukkitAudiences.create(this);
MiniMessage miniMessage = MiniMessage.builder()
.postProcessor(new AdventureLegacyColorPostProcessor())
.preProcessor(new AdventureLegacyColorPreProcessor())
.build();
BridgeService bridgeService = new BridgeService(this.pluginConfig, server.getPluginManager(), this.getLogger(), this);
bridgeService.init(this.fightManager, server);
NoticeService noticeService = new NoticeService(pluginConfig, miniMessage);
BridgeService bridgeService = new BridgeService(
pluginConfig,
server.getPluginManager(),
this.getLogger(),
this,
this.fightManager
);
bridgeService.init(server);
this.regionProvider = bridgeService.getRegionProvider();
NotificationAnnouncer notificationAnnouncer = new NotificationAnnouncer(this.audienceProvider, this.pluginConfig, miniMessage);
BorderService borderService = new BorderServiceImpl(scheduler, server, regionProvider, eventManager, () -> pluginConfig.border);
KnockbackService knockbackService = new KnockbackService(pluginConfig, scheduler, regionProvider);
this.liteCommands = LiteBukkitFactory.builder(FALLBACK_PREFIX, this, server)
.message(LiteBukkitMessages.PLAYER_NOT_FOUND, this.pluginConfig.messages.playerNotFound)
.message(LiteBukkitMessages.PLAYER_ONLY, this.pluginConfig.messages.admin.onlyForPlayers)
.message(LiteBukkitMessages.PLAYER_NOT_FOUND, pluginConfig.messagesSettings.playerNotFound)
.message(LiteBukkitMessages.PLAYER_ONLY, pluginConfig.messagesSettings.admin.onlyForPlayers)
.invalidUsage(new InvalidUsageHandlerImpl(this.pluginConfig, notificationAnnouncer))
.missingPermission(new MissingPermissionHandlerImpl(this.pluginConfig, notificationAnnouncer))
.invalidUsage(new InvalidUsageHandlerImpl(pluginConfig, noticeService))
.missingPermission(new MissingPermissionHandlerImpl(pluginConfig, noticeService))
.commands(
new FightTagCommand(this.fightManager, notificationAnnouncer, this.pluginConfig),
new FightTagOutCommand(this.fightTagOutService, notificationAnnouncer, this.pluginConfig),
new EternalCombatReloadCommand(configService, notificationAnnouncer)
new FightTagCommand(this.fightManager, noticeService, pluginConfig),
new FightTagOutCommand(this.fightTagOutService, noticeService, pluginConfig),
new EternalCombatReloadCommand(configService, noticeService)
)
.result(Notice.class, (invocation, result, chain) -> notificationAnnouncer.create()
.extension(new FoliaExtension(this))
.result(Notice.class, (invocation, result, chain) -> noticeService.create()
.viewer(invocation.sender())
.notice(result)
.send())
.build();
FightTask fightTask = new FightTask(server, this.pluginConfig, this.fightManager, notificationAnnouncer);
this.getServer().getScheduler().runTaskTimer(this, fightTask, 20L, 20L);
FightTask fightTask = new FightTask(server, pluginConfig, this.fightManager, noticeService);
scheduler.timer(fightTask, Duration.ofSeconds(1), Duration.ofSeconds(1));
new Metrics(this, BSTATS_METRICS_ID);
Stream.of(
new PercentDropModifier(this.pluginConfig.dropSettings),
new PlayersHealthDropModifier(this.pluginConfig.dropSettings, this.logoutService)
new PercentDropModifier(pluginConfig.drop),
new PlayersHealthDropModifier(pluginConfig.drop, logoutService)
).forEach(this.dropService::registerModifier);
Stream.of(
new DropController(this.dropService, this.dropKeepInventoryService, this.pluginConfig.dropSettings, this.fightManager),
new FightTagController(this.fightManager, this.pluginConfig),
new LogoutController(this.fightManager, this.logoutService, notificationAnnouncer, this.pluginConfig),
new FightUnTagController(this.fightManager, this.pluginConfig, this.logoutService),
new FightActionBlockerController(this.fightManager, notificationAnnouncer, this.pluginConfig),
new FightPearlController(this.pluginConfig.pearl, notificationAnnouncer, this.fightManager, this.fightPearlService),
new UpdaterNotificationController(updaterService, this.pluginConfig, this.audienceProvider, miniMessage),
new RegionController(notificationAnnouncer, this.regionProvider, this.fightManager, this.pluginConfig),
new FightEffectController(this.pluginConfig.effect, this.fightEffectService, this.fightManager, this.getServer()),
eventManager.subscribe(
new FightTagController(this.fightManager, pluginConfig),
new FightUnTagController(this.fightManager, pluginConfig, logoutService),
new FightBypassAdminController(server, pluginConfig),
new FightBypassPermissionController(server, pluginConfig),
new FightBypassCreativeController(server, pluginConfig),
new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig),
new PearlController(pluginConfig, this.pearlService, noticeService, fightManager),
new TridentController(pluginConfig, noticeService, this.fightManager, this.tridentService, server),
new DeathFlareController(pluginConfig, server, scheduler, this),
new DeathLightningController(pluginConfig, server),
new UpdaterNotificationController(updaterService, pluginConfig, miniMessage),
new KnockbackRegionController(noticeService, this.regionProvider, this.fightManager, knockbackService, server),
new FightEffectController(pluginConfig.effect, this.fightEffectService, this.fightManager, server),
new FightTagOutController(this.fightTagOutService),
new FightMessageController(this.fightManager, notificationAnnouncer, this.pluginConfig, this.getServer())
).forEach(listener -> this.getServer().getPluginManager().registerEvents(listener, this));
new FightMessageController(this.fightManager, noticeService, pluginConfig, server),
new BorderTriggerController(borderService, () -> pluginConfig.border, fightManager, server, scheduler),
new ParticleController(borderService, () -> pluginConfig.border.particle, scheduler, server),
new BorderBlockController(borderService, () -> pluginConfig.border.block, scheduler, server),
new EndCrystalListener(this, this.fightManager, pluginConfig),
new RespawnAnchorListener(this, this.fightManager, pluginConfig),
new FireworkController(this.fightManager, pluginConfig, noticeService),
new InventoryContainersBlocker(this.fightManager, pluginConfig, noticeService),
new SignEditingBlocker(this.fightManager, pluginConfig),
new CommandsBlocker(this.fightManager, noticeService, pluginConfig),
new ElytraBlocker(this.fightManager, pluginConfig),
new ElytraEquipBlocker(this.fightManager, noticeService, pluginConfig, server),
new FlyingBlocker(this.fightManager, pluginConfig, server),
new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig)
);
new KnockbackMountController(noticeService, this.regionProvider, this.fightManager).register(this);
eventManager.subscribe(
PlayerDeathEvent.class,
pluginConfig.drop.dropEventPriority,
new DropController(dropService, dropKeepInventoryService, pluginConfig.drop, fightManager, miniMessage)
);
eventManager.subscribe(
PlayerQuitEvent.class,
pluginConfig.combat.quitPunishmentEventPriority,
new LogoutController(this.fightManager, logoutService, noticeService, pluginConfig)
);
EternalCombatProvider.initialize(this);
this.apiInitialized = true;
long millis = started.elapsed(TimeUnit.MILLISECONDS);
this.getLogger().info("Successfully loaded EternalCombat in " + millis + "ms");
@@ -168,17 +237,18 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
@Override
public void onDisable() {
EternalCombatProvider.deinitialize();
if (this.liteCommands != null) {
this.liteCommands.unregister();
}
if (this.audienceProvider != null) {
this.audienceProvider.close();
if (this.fightManager != null) {
this.fightManager.untagAll();
}
this.fightManager.untagAll();
if (this.apiInitialized) {
EternalCombatProvider.deinitialize();
this.apiInitialized = false;
}
}
@Override
@@ -192,8 +262,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
}
@Override
public FightPearlService getFightPearlService() {
return this.fightPearlService;
public PearlService getFightPearlService() {
return this.pearlService;
}
@Override
@@ -0,0 +1,30 @@
package com.eternalcode.combat;
import com.eternalcode.commons.bukkit.scheduler.BukkitSchedulerImpl;
import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler;
import com.eternalcode.commons.folia.scheduler.FoliaSchedulerImpl;
import java.util.logging.Logger;
import org.bukkit.plugin.Plugin;
public final class CombatSchedulerAdapter {
private static final String FOLIA_CLASS = "io.papermc.paper.threadedregions.RegionizedServer";
private CombatSchedulerAdapter() {
throw new UnsupportedOperationException("This is an utility class and cannot be instantiated");
}
public static MinecraftScheduler getAdaptiveScheduler(Plugin plugin) {
Logger logger = plugin.getLogger();
try {
Class.forName(FOLIA_CLASS);
logger.info("» Detected Folia environment. Using FoliaScheduler.");
return new FoliaSchedulerImpl(plugin);
}
catch (ClassNotFoundException exception) {
logger.info("» Detected Bukkit/Paper environment. Using BukkitScheduler.");
return new BukkitSchedulerImpl(plugin);
}
}
}
@@ -1,8 +1,7 @@
package com.eternalcode.combat;
import com.eternalcode.combat.config.ConfigService;
import com.eternalcode.combat.notification.NotificationAnnouncer;
import com.eternalcode.multification.bukkit.notice.BukkitNotice;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.multification.notice.Notice;
import com.google.common.base.Stopwatch;
import dev.rollczi.litecommands.annotations.async.Async;
@@ -14,29 +13,29 @@ import java.time.Duration;
import org.bukkit.command.CommandSender;
@Command(name = "combatlog", aliases = "combat")
@Permission("eternalcombat.reload")
public class EternalCombatReloadCommand {
private static final Notice RELOAD_MESSAGE = BukkitNotice.builder()
private static final Notice RELOAD_MESSAGE = Notice.builder()
.chat("<b><gradient:#8a1212:#fc6b03>EternalCombat:</gradient></b> Reloaded EternalCombat in <color:#fce303>{TIME}ms!</color>")
.build();
private final ConfigService configService;
private final NotificationAnnouncer announcer;
private final NoticeService noticeService;
public EternalCombatReloadCommand(ConfigService configService, NotificationAnnouncer announcer) {
public EternalCombatReloadCommand(ConfigService configService, NoticeService noticeService) {
this.configService = configService;
this.announcer = announcer;
this.noticeService = noticeService;
}
@Async
@Execute(name = "reload")
@Permission("eternalcombat.reload")
void execute(@Context CommandSender sender) {
Stopwatch stopwatch = Stopwatch.createStarted();
this.configService.reload();
Duration elapsed = stopwatch.elapsed();
this.announcer.create()
this.noticeService.create()
.viewer(sender)
.notice(RELOAD_MESSAGE)
.placeholder("{TIME}", String.valueOf(elapsed.toMillis()))
@@ -0,0 +1,66 @@
package com.eternalcode.combat.border;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
class BorderActivePointsIndex {
private final Map<String, Map<UUID, Set<BorderPoint>>> registry = new ConcurrentHashMap<>();
boolean hasPoints(String world, UUID player) {
Map<UUID, Set<BorderPoint>> worldRegistry = this.registry.get(world);
if (worldRegistry == null) {
return false;
}
return worldRegistry.containsKey(player);
}
Set<BorderPoint> putPoints(String world, UUID player, Set<BorderPoint> points) {
Map<UUID, Set<BorderPoint>> worldRegistry = this.registry.computeIfAbsent(world, k -> new ConcurrentHashMap<>());
Set<BorderPoint> oldPoints = worldRegistry.put(player, points);
if (oldPoints != null) {
return calculateRemovedPoints(points, oldPoints);
}
return Set.of();
}
private static Set<BorderPoint> calculateRemovedPoints(Set<BorderPoint> points, Set<BorderPoint> oldPoints) {
Set<BorderPoint> removed = new HashSet<>();
for (BorderPoint oldPoint : oldPoints) {
if (!points.contains(oldPoint)) {
removed.add(oldPoint);
}
}
return Collections.unmodifiableSet(removed);
}
Set<BorderPoint> getPoints(String world, UUID player) {
Map<UUID, Set<BorderPoint>> worldRegistry = this.registry.get(world);
if (worldRegistry == null) {
return Set.of();
}
return worldRegistry.getOrDefault(player, Set.of());
}
Set<BorderPoint> removePoints(String world, UUID player) {
Map<UUID, Set<BorderPoint>> worldRegistry = this.registry.get(world);
if (worldRegistry == null) {
return Set.of();
}
Set<BorderPoint> remove = worldRegistry.remove(player);
if (remove == null) {
return Set.of();
}
return remove;
}
}
@@ -0,0 +1,24 @@
package com.eternalcode.combat.border;
import dev.rollczi.litecommands.shared.Lazy;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
class BorderLazyResult implements BorderResult {
private final List<Lazy<List<BorderPoint>>> borderPoints = new ArrayList<>();
void addLazyBorderPoints(Lazy<List<BorderPoint>> supplier) {
borderPoints.add(supplier);
}
@Override
public Set<BorderPoint> collect() {
return borderPoints.stream()
.flatMap(result -> result.get().stream())
.collect(Collectors.toUnmodifiableSet());
}
}
@@ -0,0 +1,25 @@
package com.eternalcode.combat.border;
import org.jetbrains.annotations.Nullable;
public record BorderPoint(int x, int y, int z, @Nullable BorderPoint innerPoint) {
public BorderPoint(int x, int y, int z) {
this(x, y, z, null);
}
/**
* This point represents an adjusted coordinate that takes into account the block's volume (thickness)
* and is used for logical placement and grouping, especially for assigning border blocks to the
* correct chunk when they are on the very edge of a chunk boundary.
* This ensures the block is considered to be on the "inner" side of the border for chunk-based logic.
*/
@Override
public BorderPoint innerPoint() {
if (this.innerPoint == null) {
return this;
}
return this.innerPoint;
}
}
@@ -0,0 +1,9 @@
package com.eternalcode.combat.border;
import java.util.Set;
public interface BorderResult {
Set<BorderPoint> collect();
}
@@ -0,0 +1,15 @@
package com.eternalcode.combat.border;
import java.util.Set;
import org.bukkit.Location;
import org.bukkit.entity.Player;
public interface BorderService {
void updateBorder(Player player, Location to);
void clearBorder(Player player);
Set<BorderPoint> getActiveBorder(Player player);
}
@@ -0,0 +1,185 @@
package com.eternalcode.combat.border;
import com.eternalcode.combat.border.event.BorderHideAsyncEvent;
import com.eternalcode.combat.border.event.BorderShowAsyncEvent;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler;
import com.eternalcode.commons.scheduler.Scheduler;
import dev.rollczi.litecommands.shared.Lazy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Player;
public class BorderServiceImpl implements BorderService {
private final MinecraftScheduler scheduler;
private final EventManager eventManager;
private final Supplier<BorderSettings> settings;
private final BorderTriggerIndex borderIndexes;
private final BorderActivePointsIndex activeBorderIndex = new BorderActivePointsIndex();
public BorderServiceImpl(MinecraftScheduler scheduler, Server server, RegionProvider provider, EventManager eventManager, Supplier<BorderSettings> settings) {
this.scheduler = scheduler;
this.eventManager = eventManager;
this.settings = settings;
this.borderIndexes = BorderTriggerIndex.started(server, scheduler, provider, settings);
}
@Override
public void updateBorder(Player player, Location location) {
Optional<BorderResult> result = resolveBorder(location);
String world = player.getWorld().getName();
if (result.isEmpty()) {
if (!this.activeBorderIndex.hasPoints(world, player.getUniqueId())) {
return;
}
this.clearBorder(player);
return;
}
scheduler.runAsync(() -> {
BorderResult borderResult = result.get();
Set<BorderPoint> points = borderResult.collect();
if (!points.isEmpty()) {
BorderShowAsyncEvent event = eventManager.publishEvent(new BorderShowAsyncEvent(player, points));
points = event.getPoints();
}
Set<BorderPoint> removed = this.activeBorderIndex.putPoints(world, player.getUniqueId(), points);
if (!removed.isEmpty()) {
eventManager.publishEvent(new BorderHideAsyncEvent(player, removed));
}
});
}
@Override
public void clearBorder(Player player) {
World world = player.getWorld();
UUID uniqueId = player.getUniqueId();
scheduler.runAsync(() -> {
Set<BorderPoint> removed = this.activeBorderIndex.removePoints(world.getName(), uniqueId);
if (!removed.isEmpty()) {
eventManager.publishEvent(new BorderHideAsyncEvent(player, removed));
}
});
}
@Override
public Set<BorderPoint> getActiveBorder(Player player) {
return this.activeBorderIndex.getPoints(player.getWorld().getName(), player.getUniqueId());
}
private Optional<BorderResult> resolveBorder(Location location) {
List<BorderTrigger> triggered = borderIndexes.getTriggered(location);
if (triggered.isEmpty()) {
return Optional.empty();
}
BorderLazyResult result = new BorderLazyResult();
for (BorderTrigger trigger : triggered) {
result.addLazyBorderPoints(new Lazy<>(() -> this.resolveBorderPoints(trigger, location)));
}
return Optional.of(result);
}
/* this code is ugly but is fast */
private List<BorderPoint> resolveBorderPoints(BorderTrigger trigger, Location playerLocation) {
BorderPoint borderMin = trigger.min();
BorderPoint borderMax = trigger.max();
int x = (int) Math.round(playerLocation.getX());
int y = (int) Math.round(playerLocation.getY());
int z = (int) Math.round(playerLocation.getZ());
int distanceRounded = settings.get().distanceRounded();
int realMinX = Math.max(borderMin.x(), x - distanceRounded);
int realMaxX = Math.min(borderMax.x(), x + distanceRounded);
int realMinY = Math.max(borderMin.y(), y - distanceRounded);
int realMaxY = Math.min(borderMax.y(), y + distanceRounded);
int realMinZ = Math.max(borderMin.z(), z - distanceRounded);
int realMaxZ = Math.min(borderMax.z(), z + distanceRounded);
List<BorderPoint> points = new ArrayList<>();
if (borderMin.y() >= realMinY) { // Bottom wall
for (int currentX = realMinX; currentX <= realMaxX - 1; currentX++) {
for (int currentZ = realMinZ; currentZ <= realMaxZ - 1; currentZ++) {
addPoint(points, currentX, realMinY, currentZ, playerLocation, null);
}
}
}
if (borderMax.y() <= realMaxY) { // Top wall
for (int currentX = realMinX; currentX <= realMaxX; currentX++) {
for (int currentZ = realMinZ; currentZ <= realMaxZ; currentZ++) {
BorderPoint innerPoint = new BorderPoint(Math.max(realMinX, currentX - 1), realMaxY - 1, Math.max(realMinZ, currentZ - 1));
addPoint(points, currentX, realMaxY, currentZ, playerLocation, innerPoint);
}
}
}
if (borderMin.x() >= realMinX) { // West wall (left)
for (int currentY = realMinY; currentY <= realMaxY - 1; currentY++) {
for (int currentZ = realMinZ; currentZ <= realMaxZ - 1; currentZ++) {
addPoint(points, realMinX, currentY, currentZ, playerLocation, null);
}
}
}
if (borderMax.x() <= realMaxX) { // East wall (right)
for (int currentY = realMinY; currentY <= realMaxY; currentY++) {
for (int currentZ = realMinZ; currentZ <= realMaxZ; currentZ++) {
BorderPoint innerPoint = new BorderPoint(realMaxX - 1, Math.max(realMinY, currentY - 1), Math.max(realMinZ, currentZ - 1));
addPoint(points, realMaxX, currentY, currentZ, playerLocation, innerPoint);
}
}
}
if (borderMin.z() >= realMinZ) { // North wall (front)
for (int currentX = realMinX; currentX <= realMaxX - 1; currentX++) {
for (int currentY = realMinY; currentY <= realMaxY - 1; currentY++) {
addPoint(points, currentX, currentY, realMinZ, playerLocation, null);
}
}
}
if (borderMax.z() <= realMaxZ) { // South wall (back)
for (int currentX = realMinX; currentX <= realMaxX; currentX++) {
for (int currentY = realMinY; currentY <= realMaxY; currentY++) {
BorderPoint innerPoint = new BorderPoint(Math.max(realMinX, currentX - 1), Math.max(realMinY, currentY - 1), realMaxZ - 1);
addPoint(points, currentX, currentY, realMaxZ, playerLocation, innerPoint);
}
}
}
return points;
}
private void addPoint(List<BorderPoint> points, int x, int y, int z, Location playerLocation, BorderPoint innerPoint) {
if (isVisible(x, y, z, playerLocation)) {
points.add(new BorderPoint(x, y, z, innerPoint));
}
}
private boolean isVisible(int x, int y, int z, Location player) {
return Math.sqrt(Math.pow(x - player.getX(), 2) + Math.pow(y - player.getY(), 2) + Math.pow(z - player.getZ(), 2)) <= this.settings.get().distance;
}
}
@@ -0,0 +1,42 @@
package com.eternalcode.combat.border;
import com.eternalcode.combat.border.animation.block.BlockSettings;
import com.eternalcode.combat.border.animation.particle.ParticleSettings;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
import org.jetbrains.annotations.ApiStatus;
public class BorderSettings extends OkaeriConfig {
@Comment("# Border view distance in blocks")
public double distance = 6.5;
@Comment({
" ",
"# Border block animation settings",
"# Configure the block animation that appears during combat."
})
public BlockSettings block = new BlockSettings();
@Comment({
" ",
"# Border particle animation settings",
"# Configure the particle animation that appears during combat."
})
public ParticleSettings particle = new ParticleSettings();
@ApiStatus.Internal
public Duration indexRefreshDelay() {
return Duration.ofSeconds(1);
}
public int distanceRounded() {
return (int) Math.ceil(this.distance);
}
public boolean isEnabled() {
return this.block.enabled || this.particle.enabled;
}
}
@@ -0,0 +1,20 @@
package com.eternalcode.combat.border;
record BorderTrigger(BorderPoint min, BorderPoint max, BorderPoint triggerMin, BorderPoint triggerMax) {
BorderTrigger(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, int distance) {
this(
new BorderPoint(minX, minY, minZ),
new BorderPoint(maxX, maxY, maxZ),
new BorderPoint(minX - distance, minY - distance, minZ - distance),
new BorderPoint(maxX + distance, maxY + distance, maxZ + distance)
);
}
public boolean isTriggered(int x, int y, int z) {
return x >= triggerMin.x() && x <= triggerMax.x()
&& y >= triggerMin.y() && y <= triggerMax.y()
&& z >= triggerMin.z() && z <= triggerMax.z();
}
}
@@ -0,0 +1,108 @@
package com.eternalcode.combat.border;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler;
import java.time.Duration;
import java.util.function.Supplier;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
public class BorderTriggerController implements Listener {
private static final long BORDER_WORK_AROUND_DELAY_MILLIS = 250;
private final BorderService borderService;
private final Supplier<BorderSettings> border;
private final FightManager fightManager;
private final Server server;
private final MinecraftScheduler scheduler;
public BorderTriggerController(
BorderService borderService,
Supplier<BorderSettings> border,
FightManager fightManager,
Server server,
MinecraftScheduler scheduler
) {
this.borderService = borderService;
this.border = border;
this.fightManager = fightManager;
this.server = server;
this.scheduler = scheduler;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
void onMove(PlayerMoveEvent event) {
if (!border.get().isEnabled()) {
return;
}
Location to = event.getTo();
Location from = event.getFrom();
if (to.getBlockX() == from.getBlockX() && to.getBlockY() == from.getBlockY() && to.getBlockZ() == from.getBlockZ()) {
return;
}
Player player = event.getPlayer();
if (!fightManager.isInCombat(player.getUniqueId())) {
return;
}
borderService.updateBorder(player, to);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
void onTeleport(PlayerTeleportEvent event) {
if (!border.get().isEnabled()) {
return;
}
Player player = event.getPlayer();
if (!fightManager.isInCombat(player.getUniqueId())) {
return;
}
borderService.updateBorder(player, event.getTo());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
void onFightStart(FightTagEvent event) {
if (!border.get().isEnabled()) {
return;
}
Player player = server.getPlayer(event.getPlayer());
if (player == null) {
return;
}
borderService.updateBorder(player, player.getLocation());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
void onFightEnd(FightUntagEvent event) {
if (!border.get().isEnabled()) {
return;
}
Player player = server.getPlayer(event.getPlayer());
if (player == null) {
return;
}
// Due to race condition in PlayerMoveEvent and FightUntagEvent - border can be triggered after death and untag of player - solution delayed removal of border
this.scheduler.runLater(() -> {
if (!this.fightManager.isInCombat(player.getUniqueId())) {
this.borderService.clearBorder(player);
}
}, Duration.ofMillis(BORDER_WORK_AROUND_DELAY_MILLIS));
}
}
@@ -0,0 +1,82 @@
package com.eternalcode.combat.border;
import com.eternalcode.combat.region.Region;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler;
import com.eternalcode.commons.scheduler.Scheduler;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
class BorderTriggerIndex {
private final Server server;
private final MinecraftScheduler scheduler;
private final RegionProvider provider;
private final Supplier<BorderSettings> settings;
private final Map<String, BorderTriggerIndexBucket> borderIndexes = new HashMap<>();
private BorderTriggerIndex(Server server, MinecraftScheduler scheduler, RegionProvider provider, Supplier<BorderSettings> settings) {
this.server = server;
this.scheduler = scheduler;
this.provider = provider;
this.settings = settings;
}
private void updateWorlds() {
for (World world : server.getWorlds()) {
this.updateWorld(world.getName(), provider.getRegions(world));
}
}
private void updateWorld(String world, Collection<Region> regions) {
this.scheduler.runAsync(() -> {
int distanceRounded = settings.get().distanceRounded();
List<BorderTrigger> triggers = new ArrayList<>();
for (Region region : regions) {
Location min = region.getMin();
Location max = region.getMax();
triggers.add(new BorderTrigger(
min.getBlockX(), min.getBlockY(), min.getBlockZ(),
max.getBlockX() + 1, max.getBlockY() + 1, max.getBlockZ() + 1,
distanceRounded
));
}
BorderTriggerIndexBucket index = BorderTriggerIndexBucket.create(triggers);
this.borderIndexes.put(world, index);
});
}
static BorderTriggerIndex started(Server server, MinecraftScheduler scheduler, RegionProvider provider, Supplier<BorderSettings> settings) {
BorderTriggerIndex index = new BorderTriggerIndex(server, scheduler, provider, settings);
scheduler.timerAsync(() -> index.updateWorlds(), Duration.ZERO, settings.get().indexRefreshDelay());
return index;
}
public List<BorderTrigger> getTriggered(Location location) {
BorderTriggerIndexBucket index = borderIndexes.get(location.getWorld().getName());
if (index == null) {
return List.of();
}
int x = (int) Math.round(location.getX());
int y = (int) Math.round(location.getY());
int z = (int) Math.round(location.getZ());
return index.getTriggers(x, z)
.stream()
.filter(trigger -> trigger.isTriggered(x, y, z))
.toList();
}
}
@@ -0,0 +1,84 @@
package com.eternalcode.combat.border;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
class BorderTriggerIndexBucket {
/**
* Represents "00000000 00000000 00000000 00000000 11111111 11111111 11111111 11111111" bit mask.
* This allows removing left bits when used with AND bit operator.
* */
private static final long LEFT_INT_MASK = 0xFFFFFFFFL;
/**
* Allows packing the coordination on one axis from 256 into 1
* Why? - We divide the world into fragments of size 256x256
* Why it is 8? Because when you shift bits, then the value will be smaller / bigger in scale 2^n
* E.g `2^4 = 16` (standard minecraft chunk size) in our case, it is `2^8 = 256`
*/
private static final int CHUNK_SHIFT = 8;
private final Map<Long, Set<BorderTrigger>> index = new HashMap<>();
private BorderTriggerIndexBucket() {
}
Set<BorderTrigger> getTriggers(int x, int z) {
long position = packChunk(x >> CHUNK_SHIFT, z >> CHUNK_SHIFT);
return this.index.getOrDefault(position, Set.of());
}
static BorderTriggerIndexBucket create(Collection<BorderTrigger> triggers) {
return new BorderTriggerIndexBucket().with(triggers);
}
private BorderTriggerIndexBucket with(Collection<BorderTrigger> triggers) {
for (BorderTrigger trigger : triggers) {
withTrigger(trigger);
}
return this;
}
private void withTrigger(BorderTrigger trigger) {
int minX = trigger.triggerMin().x() >> CHUNK_SHIFT;
int minZ = trigger.triggerMin().z() >> CHUNK_SHIFT;
int maxX = trigger.triggerMax().x() >> CHUNK_SHIFT;
int maxZ = trigger.triggerMax().z() >> CHUNK_SHIFT;
int startX = Math.min(minX, maxX);
int startZ = Math.min(minZ, maxZ);
int endX = Math.max(minX, maxX);
int endZ = Math.max(minZ, maxZ);
for (int chunkX = startX; chunkX <= endX; chunkX++) {
for (int chunkZ = startZ; chunkZ <= endZ; chunkZ++) {
long packed = packChunk(chunkX, chunkZ);
this.index.computeIfAbsent(packed, key -> new HashSet<>())
.add(trigger);
}
}
}
/**
* Packs two ints into long value.
* <p>
* For example for values:
* <ul>
* <li>X - 00000000 00000000 00000000 00000001 </li>
* <li>Z - 00000000 00000000 00000000 00000111 </li>
* </ul>
* This method will return:<br>
* 00000000 00000000 00000000 00000111 00000000 00000000 00000000 00000001
* </p>
* @param bigChunkX right int to pack
* @param bigChunkZ left int to pack
*/
private static long packChunk(int bigChunkX, int bigChunkZ) {
return (long) bigChunkX & LEFT_INT_MASK | ((long) bigChunkZ & LEFT_INT_MASK) << Integer.SIZE;
}
}
@@ -0,0 +1,18 @@
package com.eternalcode.combat.border.animation;
import java.awt.Color;
public final class BorderColorUtil {
private BorderColorUtil() {
}
public static Color xyzToRainbow(int x, int y, int z) {
float hue = (float) ((Math.sin(x * 0.05) + Math.cos(z * 0.05)) * 0.5 + 0.5);
float saturation = 1.0f;
float brightness = 0.8f + 0.2f * Math.max(0.0f, Math.min(1.0f, (float) y / 255));
return Color.getHSBColor(hue, saturation, brightness);
}
}
@@ -0,0 +1,33 @@
package com.eternalcode.combat.border.animation.block;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
public class BlockSettings extends OkaeriConfig {
@Comment("# Enable block animation?")
public boolean enabled = true;
@Comment({
"# Block type used for rendering the border",
"# Custom: RAINBOW_GLASS, RAINBOW_WOOL, RAINBOW_TERRACOTTA, RAINBOW_CONCRETE",
"# Vanilla: https://javadocs.packetevents.com/com/github/retrooper/packetevents/protocol/world/states/type/StateTypes.html"
})
public BlockType type = BlockType.RAINBOW_GLASS;
@Comment({
"# Delay between each async animation update (need restart to apply)",
"# Lower values will decrease performance but will make the animation smoother",
"# Higher values will increase performance"
})
public Duration updateDelay = Duration.ofMillis(250);
@Comment({
"# Delay between each chunk cache update (need restart to apply)",
"# Lower values will decrease performance",
"# Higher values will increase performance but may cause overlapping existing blocks (this does not modify the world)"
})
public Duration chunkCacheDelay = Duration.ofMillis(300);
}
@@ -0,0 +1,49 @@
package com.eternalcode.combat.border.animation.block;
import com.eternalcode.combat.border.BorderPoint;
import static com.eternalcode.combat.border.animation.block.BorderBlockRainbowUtil.xyzToConcrete;
import static com.eternalcode.combat.border.animation.block.BorderBlockRainbowUtil.xyzToGlass;
import static com.eternalcode.combat.border.animation.block.BorderBlockRainbowUtil.xyzToTerracotta;
import static com.eternalcode.combat.border.animation.block.BorderBlockRainbowUtil.xyzToWool;
import com.github.retrooper.packetevents.protocol.world.states.type.StateType;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
import java.util.Locale;
public class BlockType {
public static final BlockType RAINBOW_GLASS = new BlockType("RAINBOW_GLASS", point -> xyzToGlass(point));
public static final BlockType RAINBOW_TERRACOTTA = new BlockType("RAINBOW_TERRACOTTA", point -> xyzToTerracotta(point));
public static final BlockType RAINBOW_WOOL = new BlockType("RAINBOW_WOOL", point -> xyzToWool(point));
public static final BlockType RAINBOW_CONCRETE = new BlockType("RAINBOW_CONCRETE", point -> xyzToConcrete(point));
private final String name;
private final TypeProvider type;
private BlockType(String name, TypeProvider type) {
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public StateType getStateType(BorderPoint point) {
return type.provide(point);
}
public static BlockType fromName(String name) {
return switch (name) {
case "RAINBOW_GLASS" -> RAINBOW_GLASS;
case "RAINBOW_WOOL" -> RAINBOW_WOOL;
case "RAINBOW_TERRACOTTA" -> RAINBOW_TERRACOTTA;
case "RAINBOW_CONCRETE" -> RAINBOW_CONCRETE;
default -> new BlockType(name, point -> StateTypes.getByName(name.toLowerCase(Locale.ROOT)));
};
}
private interface TypeProvider {
StateType provide(BorderPoint point);
}
}
@@ -0,0 +1,30 @@
package com.eternalcode.combat.border.animation.block;
import eu.okaeri.configs.schema.GenericsPair;
import eu.okaeri.configs.serdes.BidirectionalTransformer;
import eu.okaeri.configs.serdes.SerdesContext;
import java.util.Locale;
public class BlockTypeTransformer extends BidirectionalTransformer<String, BlockType> {
@Override
public GenericsPair<String, BlockType> getPair() {
return this.genericsPair(String.class, BlockType.class);
}
@Override
public BlockType leftToRight(String data, SerdesContext serdesContext) {
BlockType blockType = BlockType.fromName(data);
if (blockType == null) {
throw new IllegalArgumentException("Unknown block type: " + data);
}
return blockType;
}
@Override
public String rightToLeft(BlockType data, SerdesContext serdesContext) {
return data.getName().toUpperCase(Locale.ROOT);
}
}
@@ -0,0 +1,231 @@
package com.eternalcode.combat.border.animation.block;
import com.eternalcode.combat.border.BorderPoint;
import com.eternalcode.combat.border.BorderService;
import com.eternalcode.combat.border.event.BorderHideAsyncEvent;
import com.eternalcode.combat.border.event.BorderShowAsyncEvent;
import com.eternalcode.commons.scheduler.Scheduler;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.PacketEventsAPI;
import com.github.retrooper.packetevents.manager.player.PlayerManager;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
import com.github.retrooper.packetevents.protocol.world.states.type.StateType;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
import com.github.retrooper.packetevents.util.Vector3i;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange.EncodedBlock;
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Server;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
public class BorderBlockController implements Listener {
public static final PacketEventsAPI<?> PACKET_EVENTS = PacketEvents.getAPI();
public static final PlayerManager PLAYER_MANAGER = PACKET_EVENTS.getPlayerManager();
public static final ClientVersion SERVER_VERSION = PACKET_EVENTS.getServerManager().getVersion().toClientVersion();
public static final WrappedBlockState AIR_ID = WrappedBlockState.getDefaultState(SERVER_VERSION, StateTypes.AIR);
public static final int MINECRAFT_CHUNK_SHIFT = 4;
private final BorderService borderService;
private final Supplier<BlockSettings> settings;
private final Server server;
private final Set<UUID> playersToUpdate = ConcurrentHashMap.newKeySet();
private final Map<UUID, Object> lockedPlayers = new ConcurrentHashMap<>();
private final Map<UUID, Map<BorderPoint, WrappedBlockState>> originalBlocks = new ConcurrentHashMap<>();
private final ChunkCache chunkCache;
public BorderBlockController(BorderService borderService, Supplier<BlockSettings> settings, Scheduler scheduler, Server server) {
this.borderService = borderService;
this.settings = settings;
this.server = server;
this.chunkCache = new ChunkCache(settings.get());
scheduler.timerAsync(() -> this.updatePlayers(), settings.get().updateDelay, settings.get().updateDelay);
}
@EventHandler
void onBorderShowAsyncEvent(BorderShowAsyncEvent event) {
if (!this.settings.get().enabled) {
return;
}
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
synchronized (this.getLock(playerId)) {
Set<BorderPoint> borderPoints = this.savePassablePoints(player, event.getPoints());
event.setPoints(borderPoints);
this.showBorder(player, borderPoints);
this.playersToUpdate.add(playerId);
}
}
@EventHandler
void onBorderHideAsyncEvent(BorderHideAsyncEvent event) {
if (!this.settings.get().enabled) {
return;
}
UUID playerId = event.getPlayer().getUniqueId();
synchronized (this.getLock(playerId)) {
this.hideBorder(event.getPlayer(), event.getPoints());
Set<BorderPoint> border = this.borderService.getActiveBorder(event.getPlayer());
if (border.isEmpty()) {
this.playersToUpdate.remove(playerId);
this.originalBlocks.remove(playerId);
}
}
}
@EventHandler
void onPlayerQuit(PlayerQuitEvent event) {
UUID playerId = event.getPlayer().getUniqueId();
this.playersToUpdate.remove(playerId);
this.originalBlocks.remove(playerId);
this.lockedPlayers.remove(playerId);
}
private void updatePlayers() {
if (!this.settings.get().enabled) {
return;
}
for (UUID uuid : this.playersToUpdate) {
Player player = this.server.getPlayer(uuid);
if (player == null) {
this.playersToUpdate.remove(uuid);
continue;
}
this.updatePlayer(uuid, player);
}
}
private void updatePlayer(UUID uuid, Player player) {
synchronized (this.getLock(uuid)) {
Set<BorderPoint> border = this.borderService.getActiveBorder(player);
if (border.isEmpty()) {
this.playersToUpdate.remove(uuid);
this.originalBlocks.remove(uuid);
return;
}
Set<BorderPoint> passablePoints = this.savePassablePoints(player, border);
this.showBorder(player, passablePoints);
}
}
private void showBorder(Player player, Collection<BorderPoint> blocks) {
this.groupByChunkPacket(blocks).forEach((chunkPos, chunkPoints) -> {
EncodedBlock[] encodedBlocks = chunkPoints.stream()
.map(point -> toBorderBlock(point))
.toArray(EncodedBlock[]::new);
PLAYER_MANAGER.sendPacket(player, new WrapperPlayServerMultiBlockChange(chunkPos, true, encodedBlocks));
});
}
private void hideBorder(Player player, Collection<BorderPoint> blocks) {
UUID playerId = player.getUniqueId();
Map<BorderPoint, WrappedBlockState> savedBlocks = this.originalBlocks.get(playerId);
this.groupByChunkPacket(blocks).forEach((chunkPosition, chunkPoints) -> {
EncodedBlock[] originalBlocks = chunkPoints.stream()
.map(point -> this.toOriginalBlock(point, savedBlocks))
.toArray(EncodedBlock[]::new);
PLAYER_MANAGER.sendPacket(player, new WrapperPlayServerMultiBlockChange(chunkPosition, true, originalBlocks));
});
if (savedBlocks != null) {
blocks.forEach(key -> savedBlocks.remove(key));
}
}
private Set<BorderPoint> savePassablePoints(Player player, Collection<BorderPoint> points) {
UUID playerId = player.getUniqueId();
Map<BorderPoint, WrappedBlockState> savedBlocks = this.originalBlocks.computeIfAbsent(playerId, key -> new ConcurrentHashMap<>());
return this.groupByChunk(points).entrySet().stream()
.flatMap(entry -> this.savePassableBlock(player, entry, savedBlocks))
.collect(Collectors.toSet());
}
private Stream<BorderPoint> savePassableBlock(Player player, Entry<ChunkLocation, Set<BorderPoint>> entry, Map<BorderPoint, WrappedBlockState> savedBlocks) {
ChunkSnapshot snapshot = this.chunkCache.loadSnapshot(player, entry.getKey());
if (snapshot == null) {
return Stream.empty();
}
ChunkLocation chunk = entry.getKey();
return entry.getValue().stream().filter(point -> this.saveBlockIfPassable(point, chunk, snapshot, savedBlocks));
}
private boolean saveBlockIfPassable(BorderPoint point, ChunkLocation chunk, ChunkSnapshot snapshot, Map<BorderPoint, WrappedBlockState> savedBlocks) {
int localX = point.x() - (chunk.x() << MINECRAFT_CHUNK_SHIFT);
int localZ = point.z() - (chunk.z() << MINECRAFT_CHUNK_SHIFT);
BlockData blockData = snapshot.getBlockData(localX, point.y(), localZ);
if (blockData.getMaterial().isSolid()) {
return false;
}
savedBlocks.put(point, SpigotConversionUtil.fromBukkitBlockData(blockData));
return true;
}
private EncodedBlock toBorderBlock(BorderPoint point) {
StateType type = this.settings.get().type.getStateType(point);
int blockId = WrappedBlockState.getDefaultState(SERVER_VERSION, type).getGlobalId();
return new EncodedBlock(blockId, point.x(), point.y(), point.z());
}
private EncodedBlock toOriginalBlock(BorderPoint point, Map<BorderPoint, WrappedBlockState> savedBlocks) {
WrappedBlockState blockId = savedBlocks != null ? savedBlocks.getOrDefault(point, AIR_ID) : AIR_ID;
return new EncodedBlock(blockId, point.x(), point.y(), point.z());
}
private Map<ChunkLocation, Set<BorderPoint>> groupByChunk(Collection<BorderPoint> points) {
return points.stream()
.map(borderPoint -> borderPoint.innerPoint())
.collect(Collectors.groupingBy(point -> new ChunkLocation(point.x() >> MINECRAFT_CHUNK_SHIFT, point.z() >> MINECRAFT_CHUNK_SHIFT),
Collectors.toSet()
));
}
private Map<Vector3i, Set<BorderPoint>> groupByChunkPacket(Collection<BorderPoint> points) {
return points.stream().collect(Collectors.groupingBy(
point -> new Vector3i(point.x() >> MINECRAFT_CHUNK_SHIFT, point.y() >> MINECRAFT_CHUNK_SHIFT, point.z() >> MINECRAFT_CHUNK_SHIFT),
Collectors.toSet()
));
}
private Object getLock(UUID uuid) {
return this.lockedPlayers.computeIfAbsent(uuid, k -> new Object());
}
}
@@ -0,0 +1,122 @@
package com.eternalcode.combat.border.animation.block;
import com.eternalcode.combat.border.BorderPoint;
import com.eternalcode.combat.border.animation.BorderColorUtil;
import com.github.retrooper.packetevents.protocol.world.states.type.StateType;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
import com.google.common.collect.ImmutableMap;
import java.awt.Color;
import java.util.EnumMap;
import java.util.Map;
import org.bukkit.DyeColor;
final class BorderBlockRainbowUtil {
private final static Map<DyeColor, StateType> GLASSES = new EnumMap<>(ImmutableMap.<DyeColor, StateType>builder()
.put(DyeColor.WHITE, StateTypes.WHITE_STAINED_GLASS)
.put(DyeColor.ORANGE, StateTypes.ORANGE_STAINED_GLASS)
.put(DyeColor.MAGENTA, StateTypes.MAGENTA_STAINED_GLASS)
.put(DyeColor.LIGHT_BLUE, StateTypes.LIGHT_BLUE_STAINED_GLASS)
.put(DyeColor.YELLOW, StateTypes.YELLOW_STAINED_GLASS)
.put(DyeColor.LIME, StateTypes.LIME_STAINED_GLASS)
.put(DyeColor.PINK, StateTypes.PINK_STAINED_GLASS)
.put(DyeColor.GRAY, StateTypes.GRAY_STAINED_GLASS)
.put(DyeColor.LIGHT_GRAY, StateTypes.LIGHT_GRAY_STAINED_GLASS)
.put(DyeColor.CYAN, StateTypes.CYAN_STAINED_GLASS)
.put(DyeColor.PURPLE, StateTypes.PURPLE_STAINED_GLASS)
.put(DyeColor.BLUE, StateTypes.BLUE_STAINED_GLASS)
.put(DyeColor.BROWN, StateTypes.BROWN_STAINED_GLASS)
.put(DyeColor.GREEN, StateTypes.GREEN_STAINED_GLASS)
.put(DyeColor.RED, StateTypes.RED_STAINED_GLASS)
.build()
);
private final static Map<DyeColor, StateType> TERRACOTTA = new EnumMap<>(ImmutableMap.<DyeColor, StateType>builder()
.put(DyeColor.WHITE, StateTypes.WHITE_TERRACOTTA)
.put(DyeColor.ORANGE, StateTypes.ORANGE_TERRACOTTA)
.put(DyeColor.MAGENTA, StateTypes.MAGENTA_TERRACOTTA)
.put(DyeColor.LIGHT_BLUE, StateTypes.LIGHT_BLUE_TERRACOTTA)
.put(DyeColor.YELLOW, StateTypes.YELLOW_TERRACOTTA)
.put(DyeColor.LIME, StateTypes.LIME_TERRACOTTA)
.put(DyeColor.PINK, StateTypes.PINK_TERRACOTTA)
.put(DyeColor.GRAY, StateTypes.GRAY_TERRACOTTA)
.put(DyeColor.LIGHT_GRAY, StateTypes.LIGHT_GRAY_TERRACOTTA)
.put(DyeColor.CYAN, StateTypes.CYAN_TERRACOTTA)
.put(DyeColor.PURPLE, StateTypes.PURPLE_TERRACOTTA)
.put(DyeColor.BLUE, StateTypes.BLUE_TERRACOTTA)
.put(DyeColor.BROWN, StateTypes.BROWN_TERRACOTTA)
.put(DyeColor.GREEN, StateTypes.GREEN_TERRACOTTA)
.put(DyeColor.RED, StateTypes.RED_TERRACOTTA)
.build()
);
private final static Map<DyeColor, StateType> WOOLS = new EnumMap<>(ImmutableMap.<DyeColor, StateType>builder()
.put(DyeColor.WHITE, StateTypes.WHITE_WOOL)
.put(DyeColor.ORANGE, StateTypes.ORANGE_WOOL)
.put(DyeColor.MAGENTA, StateTypes.MAGENTA_WOOL)
.put(DyeColor.LIGHT_BLUE, StateTypes.LIGHT_BLUE_WOOL)
.put(DyeColor.YELLOW, StateTypes.YELLOW_WOOL)
.put(DyeColor.LIME, StateTypes.LIME_WOOL)
.put(DyeColor.PINK, StateTypes.PINK_WOOL)
.put(DyeColor.GRAY, StateTypes.GRAY_WOOL)
.put(DyeColor.LIGHT_GRAY, StateTypes.LIGHT_GRAY_WOOL)
.put(DyeColor.CYAN, StateTypes.CYAN_WOOL)
.put(DyeColor.PURPLE, StateTypes.PURPLE_WOOL)
.put(DyeColor.BLUE, StateTypes.BLUE_WOOL)
.put(DyeColor.BROWN, StateTypes.BROWN_WOOL)
.put(DyeColor.GREEN, StateTypes.GREEN_WOOL)
.put(DyeColor.RED, StateTypes.RED_WOOL)
.build()
);
private final static Map<DyeColor, StateType> CONCRETE = new EnumMap<>(ImmutableMap.<DyeColor, StateType>builder()
.put(DyeColor.WHITE, StateTypes.WHITE_CONCRETE)
.put(DyeColor.ORANGE, StateTypes.ORANGE_CONCRETE)
.put(DyeColor.MAGENTA, StateTypes.MAGENTA_CONCRETE)
.put(DyeColor.LIGHT_BLUE, StateTypes.LIGHT_BLUE_CONCRETE)
.put(DyeColor.YELLOW, StateTypes.YELLOW_CONCRETE)
.put(DyeColor.LIME, StateTypes.LIME_CONCRETE)
.put(DyeColor.PINK, StateTypes.PINK_CONCRETE)
.put(DyeColor.GRAY, StateTypes.GRAY_CONCRETE)
.put(DyeColor.LIGHT_GRAY, StateTypes.LIGHT_GRAY_CONCRETE)
.put(DyeColor.CYAN, StateTypes.CYAN_CONCRETE)
.put(DyeColor.PURPLE, StateTypes.PURPLE_CONCRETE)
.put(DyeColor.BLUE, StateTypes.BLUE_CONCRETE)
.put(DyeColor.BROWN, StateTypes.BROWN_CONCRETE)
.put(DyeColor.GREEN, StateTypes.GREEN_CONCRETE)
.put(DyeColor.RED, StateTypes.RED_CONCRETE)
.build()
);
static StateType xyzToGlass(BorderPoint point) {
return GLASSES.get(xyzToDye(point));
}
static StateType xyzToTerracotta(BorderPoint point) {
return TERRACOTTA.get(xyzToDye(point));
}
public static StateType xyzToWool(BorderPoint point) {
return WOOLS.get(xyzToDye(point));
}
public static StateType xyzToConcrete(BorderPoint point) {
return CONCRETE.get(xyzToDye(point));
}
private static DyeColor xyzToDye(BorderPoint point) {
Color rgb = BorderColorUtil.xyzToRainbow(point.x(), point.y(), point.z());
int distance = Integer.MAX_VALUE;
DyeColor dye = DyeColor.WHITE;
for (DyeColor currentDye : DyeColor.values()) {
org.bukkit.Color color = currentDye.getColor();
int currentDistance = Math.abs(color.getRed() - rgb.getRed()) + Math.abs(color.getGreen() - rgb.getGreen()) + Math.abs(color.getBlue() - rgb.getBlue());
if (currentDistance < distance) {
distance = currentDistance;
dye = currentDye;
}
}
return dye;
}
}
@@ -0,0 +1,32 @@
package com.eternalcode.combat.border.animation.block;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.bukkit.ChunkSnapshot;
import org.bukkit.entity.Player;
class ChunkCache {
private final Cache<ChunkLocation, ChunkSnapshot> chunkCache;
ChunkCache(BlockSettings settings) {
this.chunkCache = CacheBuilder.newBuilder()
.expireAfterWrite(settings.chunkCacheDelay)
.build();
}
public ChunkSnapshot loadSnapshot(Player player, ChunkLocation location) {
ChunkSnapshot snapshot = chunkCache.getIfPresent(location);
if (snapshot != null) {
return snapshot;
}
ChunkSnapshot chunkSnapshot = player.getWorld().getChunkAtAsync(location.x(), location.z(), false)
.thenApply(chunk -> chunk.getChunkSnapshot())
.join();
chunkCache.put(location, chunkSnapshot);
return chunkSnapshot;
}
}
@@ -0,0 +1,4 @@
package com.eternalcode.combat.border.animation.block;
record ChunkLocation(int x, int z) {
}
@@ -0,0 +1,40 @@
package com.eternalcode.combat.border.animation.particle;
import com.eternalcode.combat.border.BorderPoint;
import com.eternalcode.combat.border.animation.BorderColorUtil;
import java.awt.Color;
public class ParticleColor {
public static final ParticleColor RAINBOW = new ParticleColor("RAINBOW", point -> BorderColorUtil.xyzToRainbow(point.x(), point.y(), point.z()));
private final String name;
private final ColorProvider color;
private ParticleColor(String name, ColorProvider color) {
this.name = name;
this.color = color;
}
public String getName() {
return name;
}
public Color getColor(BorderPoint point) {
return color.provide(point);
}
public static ParticleColor fromName(String name) {
if (name.equals(RAINBOW.name)) {
return RAINBOW;
}
Color decoded = Color.decode(name);
return new ParticleColor(name, point -> decoded);
}
private interface ColorProvider {
Color provide(BorderPoint point);
}
}
@@ -0,0 +1,24 @@
package com.eternalcode.combat.border.animation.particle;
import eu.okaeri.configs.schema.GenericsPair;
import eu.okaeri.configs.serdes.BidirectionalTransformer;
import eu.okaeri.configs.serdes.SerdesContext;
public class ParticleColorTransformer extends BidirectionalTransformer<String, ParticleColor> {
@Override
public GenericsPair<String, ParticleColor> getPair() {
return this.genericsPair(String.class, ParticleColor.class);
}
@Override
public ParticleColor leftToRight(String data, SerdesContext serdesContext) {
return ParticleColor.fromName(data);
}
@Override
public String rightToLeft(ParticleColor data, SerdesContext serdesContext) {
return data.getName();
}
}
@@ -0,0 +1,95 @@
package com.eternalcode.combat.border.animation.particle;
import com.eternalcode.combat.border.BorderPoint;
import com.eternalcode.combat.border.BorderService;
import com.eternalcode.combat.border.event.BorderHideAsyncEvent;
import com.eternalcode.combat.border.event.BorderShowAsyncEvent;
import com.eternalcode.commons.scheduler.Scheduler;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.PacketEventsAPI;
import com.github.retrooper.packetevents.manager.player.PlayerManager;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerParticle;
import java.time.Duration;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class ParticleController implements Listener {
private static final PacketEventsAPI<?> PACKET_EVENTS_API = PacketEvents.getAPI();
private static final PlayerManager PLAYER_MANAGER = PACKET_EVENTS_API.getPlayerManager();
private final BorderService borderService;
private final Supplier<ParticleSettings> particleSettings;
private final Server server;
private final Set<UUID> playersToUpdate = ConcurrentHashMap.newKeySet();
public ParticleController(BorderService borderService, Supplier<ParticleSettings> particleSettings, Scheduler scheduler, Server server) {
this.borderService = borderService;
this.particleSettings = particleSettings;
this.server = server;
scheduler.timerAsync(() -> this.updatePlayers(), Duration.ofMillis(200), Duration.ofMillis(200));
}
@EventHandler
void onBorderShowAsyncEvent(BorderShowAsyncEvent event) {
if (!particleSettings.get().enabled) {
return;
}
this.playersToUpdate.add(event.getPlayer().getUniqueId());
for (BorderPoint point : event.getPoints()) {
this.playParticle(event.getPlayer(), point);
}
}
@EventHandler
void onBorderHideAsyncEvent(BorderHideAsyncEvent event) {
if (!particleSettings.get().enabled) {
return;
}
Set<BorderPoint> border = this.borderService.getActiveBorder(event.getPlayer());
if (border.isEmpty()) {
this.playersToUpdate.remove(event.getPlayer().getUniqueId());
}
}
private void updatePlayers() {
if (!particleSettings.get().enabled) {
return;
}
for (UUID uuid : this.playersToUpdate) {
Player player = this.server.getPlayer(uuid);
if (player == null || !player.isOnline()) {
this.playersToUpdate.remove(uuid);
continue;
}
Set<BorderPoint> border = this.borderService.getActiveBorder(player);
if (border.isEmpty()) {
this.playersToUpdate.remove(uuid);
continue;
}
for (BorderPoint point : border) {
this.playParticle(player, point);
}
}
}
private void playParticle(Player player, BorderPoint point) {
WrapperPlayServerParticle particle = particleSettings.get().getParticle(point);
PLAYER_MANAGER.sendPacket(player, particle);
}
}
@@ -0,0 +1,76 @@
package com.eternalcode.combat.border.animation.particle;
import com.eternalcode.combat.border.BorderPoint;
import com.github.retrooper.packetevents.protocol.color.AlphaColor;
import com.github.retrooper.packetevents.protocol.particle.Particle;
import com.github.retrooper.packetevents.protocol.particle.data.ParticleColorData;
import com.github.retrooper.packetevents.protocol.particle.data.ParticleData;
import com.github.retrooper.packetevents.protocol.particle.data.ParticleDustData;
import com.github.retrooper.packetevents.protocol.particle.type.ParticleType;
import com.github.retrooper.packetevents.protocol.particle.type.ParticleTypes;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3f;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerParticle;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.awt.Color;
@SuppressWarnings("unchecked")
public class ParticleSettings extends OkaeriConfig {
@Comment("# Enable particle animation?")
public boolean enabled = true;
@Comment("# Particle type - https://javadocs.packetevents.com/com/github/retrooper/packetevents/protocol/particle/type/ParticleTypes.html")
public ParticleType type = ParticleTypes.DUST;
@Comment({
"# Particle color (used only for DUST or ENTITY_EFFECT particle type)",
"# You can set hex color e.g. \"#ca4c45\" or use \"RAINBOW\" to generate rainbow gradient based on x and z coordinates."
})
public ParticleColor color = ParticleColor.RAINBOW;
public int count = 1;
public float scale = 1.7F;
public float maxSpeed = 0.0F;
public float offsetX = 0.2F;
public float offsetY = 0.2F;
public float offsetZ = 0.2F;
public WrapperPlayServerParticle getParticle(BorderPoint point) {
return getParticle(point, type);
}
private <T extends ParticleData> WrapperPlayServerParticle getParticle(BorderPoint point, ParticleType<T> type) {
T particleData = this.createData(type, point);
Particle<?> dust = new Particle<>(type, particleData);
return new WrapperPlayServerParticle(
dust,
true,
new Vector3d(point.x(), point.y(), point.z()),
new Vector3f(orElse(offsetX, 0.1F), orElse(offsetY, 0.1F), orElse(offsetZ, 0.1F)),
orElse(maxSpeed, 0.0F),
count,
true
);
}
private <T> T orElse(T nullable, T or) {
return nullable != null ? nullable : or;
}
@SuppressWarnings("unchecked")
private <T extends ParticleData> T createData(ParticleType<T> type, BorderPoint point) {
if (type.equals(ParticleTypes.DUST)) {
Color color = this.color.getColor(point);
return (T) new ParticleDustData(orElse(scale, 1.0F), color.getRed(), color.getGreen(), color.getBlue());
}
if (type.equals(ParticleTypes.ENTITY_EFFECT)) {
Color color = this.color.getColor(point);
AlphaColor alphaColor = new AlphaColor(color.getAlpha(), color.getRed(), color.getGreen(), color.getBlue());
return (T) new ParticleColorData(alphaColor);
}
return ParticleData.emptyData();
}
}
@@ -0,0 +1,43 @@
package com.eternalcode.combat.border.animation.particle;
import com.github.retrooper.packetevents.protocol.particle.type.ParticleType;
import com.github.retrooper.packetevents.protocol.particle.type.ParticleTypes;
import com.github.retrooper.packetevents.resources.ResourceLocation;
import eu.okaeri.configs.schema.GenericsDeclaration;
import eu.okaeri.configs.schema.GenericsPair;
import eu.okaeri.configs.serdes.BidirectionalTransformer;
import eu.okaeri.configs.serdes.SerdesContext;
import java.util.Locale;
public class ParticleTypeTransformer extends BidirectionalTransformer<String, ParticleType<?>> {
@Override
public GenericsPair<String, ParticleType<?>> getPair() {
return this.generics(GenericsDeclaration.of(String.class), GenericsDeclaration.of(ParticleType.class));
}
@Override
public ParticleType<?> leftToRight(String data, SerdesContext serdesContext) {
if (!data.contains(":")) {
data = ResourceLocation.normString(data);
}
ParticleType<?> type = ParticleTypes.getByName(data.toLowerCase(Locale.ROOT));
if (type == null) {
throw new IllegalArgumentException("Unknown particle type: " + data);
}
return type;
}
@Override
public String rightToLeft(ParticleType<?> data, SerdesContext serdesContext) {
ResourceLocation location = data.getName();
if (location.getNamespace().equals(ResourceLocation.VANILLA_NAMESPACE)) {
return location.getKey().toUpperCase(Locale.ROOT);
}
return location.toString();
}
}
@@ -0,0 +1,41 @@
package com.eternalcode.combat.border.event;
import com.eternalcode.combat.border.BorderPoint;
import java.util.Set;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class BorderHideAsyncEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final Player player;
private final Set<BorderPoint> points;
public BorderHideAsyncEvent(Player player, Set<BorderPoint> points) {
super(true);
this.player = player;
this.points = points;
}
public Player getPlayer() {
return this.player;
}
public Set<BorderPoint> getPoints() {
return this.points;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}
@@ -0,0 +1,46 @@
package com.eternalcode.combat.border.event;
import com.eternalcode.combat.border.BorderPoint;
import java.util.Set;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class BorderShowAsyncEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final Player player;
private Set<BorderPoint> points;
public BorderShowAsyncEvent(Player player, Set<BorderPoint> points) {
super(true);
this.player = player;
this.points = points;
}
public Player getPlayer() {
return player;
}
public Set<BorderPoint> getPoints() {
return points;
}
public void setPoints(Set<BorderPoint> points) {
this.points = points;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}
@@ -1,56 +1,86 @@
package com.eternalcode.combat.bridge;
import com.eternalcode.combat.region.lands.LandsRegionProvider;
import com.eternalcode.combat.bridge.placeholder.FightTagPlaceholder;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.region.DefaultRegionProvider;
import com.eternalcode.combat.region.CompositeRegionProvider;
import com.eternalcode.combat.region.bukkit.DefaultRegionProvider;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.region.WorldGuardRegionProvider;
import com.eternalcode.combat.region.worldguard.WorldGuardRegionProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.bukkit.Server;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import java.util.logging.Logger;
public class BridgeService {
private final PluginConfig pluginConfig;
private final PluginConfig config;
private final PluginManager pluginManager;
private final Logger logger;
private final Plugin plugin;
private final FightManager fightManager;
private RegionProvider regionProvider;
public BridgeService(PluginConfig pluginConfig, PluginManager pluginManager, Logger logger, Plugin plugin) {
this.pluginConfig = pluginConfig;
public BridgeService(
PluginConfig config,
PluginManager pluginManager,
Logger logger,
Plugin plugin,
FightManager fightManager
) {
this.config = config;
this.pluginManager = pluginManager;
this.logger = logger;
this.plugin = plugin;
this.fightManager = fightManager;
}
public void init(FightManager fightManager, Server server) {
this.initialize("WorldGuard",
() -> this.regionProvider = new WorldGuardRegionProvider(this.pluginConfig.settings.blockedRegions, this.pluginConfig),
() -> {
this.regionProvider = new DefaultRegionProvider(this.pluginConfig.settings.blockedRegionRadius);
public void init(Server server) {
List<RegionProvider> providers = new ArrayList<>();
this.logger.warning("WorldGuard is not installed, set to default region provider.");
});
this.initialize(
"Lands",
() -> providers.add(new LandsRegionProvider(plugin)),
() -> this.logger.warning("Lands not found; skipping LandsRegionProvider.")
);
this.initialize("PlaceholderAPI",
() -> new FightTagPlaceholder(fightManager, server, this.plugin).register(),
() -> this.logger.warning("PlaceholderAPI is not installed, placeholders will not be registered.")
this.initialize(
"WorldGuard",
() -> providers.add( new WorldGuardRegionProvider(this.config)),
() -> this.logger.warning("WorldGuard not found; skipping WorldGuardRegionProvider.")
);
if (providers.isEmpty()) {
providers.add(new DefaultRegionProvider(this.config.regions.restrictedRegionRadius));
this.logger.warning("No region plugin found; using DefaultRegionProvider.");
}
this.regionProvider = new CompositeRegionProvider(providers);
this.logger.info("Using composite region provider with: " +
providers.stream()
.map(p -> p.getClass().getSimpleName())
.collect(Collectors.joining(", "))
);
initialize(
"PlaceholderAPI",
() -> new FightTagPlaceholder(this.config, this.fightManager, server, this.plugin).register(),
() -> this.logger.warning("PlaceholderAPI not found; skipping placeholders.")
);
}
private void initialize(String pluginName, BridgeInitializer initializer, Runnable failureHandler) {
private void initialize(String pluginName, BridgeInitializer initializer, Runnable onFailure) {
if (this.pluginManager.isPluginEnabled(pluginName)) {
initializer.initialize();
this.logger.info("Successfully initialized " + pluginName + " bridge.");
this.logger.info("Initialized " + pluginName + " bridge.");
}
else {
failureHandler.run();
onFailure.run();
}
}
@@ -1,5 +1,7 @@
package com.eternalcode.combat.bridge.placeholder;
import com.eternalcode.combat.config.implementation.PlaceholderSettings;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.FightTag;
import com.eternalcode.combat.util.DurationUtil;
@@ -14,23 +16,93 @@ import org.jetbrains.annotations.NotNull;
public class FightTagPlaceholder extends PlaceholderExpansion {
private static final String IDENTIFIER = "eternalcombat";
private final PlaceholderSettings placeholderSettings;
private final FightManager fightManager;
private final Server server;
private final Plugin plugin;
private static final String IDENTIFIER = "eternalcombat";
public FightTagPlaceholder(FightManager fightManager, Server server, Plugin plugin) {
public FightTagPlaceholder(PluginConfig pluginConfig, FightManager fightManager, Server server, Plugin plugin) {
this.placeholderSettings = pluginConfig.placeholders;
this.fightManager = fightManager;
this.server = server;
this.plugin = plugin;
}
@Override
public String onRequest(OfflinePlayer player, String identifier) {
return switch (identifier) {
case "remaining_millis" -> this.handleRemainingMillis(player);
case "remaining_seconds" -> this.handleRemainingSeconds(player);
case "opponent" -> this.handleOpponent(player);
case "opponent_health" -> this.handleOpponentHealth(player);
case "isInCombat" -> this.handleIsInCombat(player);
case "isInCombat_formatted" -> this.handleIsInCombatFormatted(player);
default -> null;
};
}
private String handleRemainingMillis(OfflinePlayer player) {
return this.getFightTag(player)
.map(tag -> DurationParser.TIME_UNITS.format(tag.getRemainingDuration()))
.orElse("");
}
private String handleRemainingSeconds(OfflinePlayer player) {
return this.getFightTag(player)
.map(tag -> DurationUtil.format(tag.getRemainingDuration()))
.orElse("");
}
private String handleOpponent(OfflinePlayer player) {
return this.getTagger(player)
.map(Player::getName)
.orElse("");
}
private String handleOpponentHealth(OfflinePlayer player) {
return this.getTagger(player)
.map(tagger -> String.format("%.2f", tagger.getHealth()))
.orElse("");
}
private String handleIsInCombat(OfflinePlayer player) {
return String.valueOf(this.isPlayerInCombat(player));
}
private String handleIsInCombatFormatted(OfflinePlayer player) {
return this.isPlayerInCombat(player)
? this.placeholderSettings.isInCombatFormattedTrue
: this.placeholderSettings.isInCombatFormattedFalse;
}
private boolean isPlayerInCombat(OfflinePlayer player) {
Player onlinePlayer = player.getPlayer();
return onlinePlayer != null && this.fightManager.isInCombat(onlinePlayer.getUniqueId());
}
private @NotNull Optional<Player> getTagger(OfflinePlayer player) {
return this.getFightTag(player)
.map(FightTag::getTagger)
.map(this.server::getPlayer);
}
private Optional<FightTag> getFightTag(OfflinePlayer player) {
Player onlinePlayer = player.getPlayer();
if (onlinePlayer == null || !this.fightManager.isInCombat(onlinePlayer.getUniqueId())) {
return Optional.empty();
}
return Optional.of(this.fightManager.getTag(onlinePlayer.getUniqueId()));
}
@Override
public boolean canRegister() {
return true;
}
@Override
public @NotNull String getIdentifier() {
return IDENTIFIER;
@@ -45,54 +117,4 @@ public class FightTagPlaceholder extends PlaceholderExpansion {
public @NotNull String getVersion() {
return this.plugin.getDescription().getVersion();
}
@Override
public String onRequest(OfflinePlayer player, String identifier) {
if (identifier.equals("remaining_seconds")) {
return this.getFightTag(player)
.map(fightTagInter -> DurationUtil.format(fightTagInter.getRemainingDuration()))
.orElse("");
}
if (identifier.equals("remaining_millis")) {
return this.getFightTag(player)
.map(fightTag -> DurationParser.TIME_UNITS.format(fightTag.getRemainingDuration()))
.orElse("");
}
if (identifier.equals("opponent")) {
return this.getTagger(player)
.map(tagger -> tagger.getName())
.orElse("");
}
if (identifier.equals("opponent_health")) {
return this.getTagger(player)
.map(tagger -> String.format("%.2f", tagger.getHealth()))
.orElse("");
}
return null;
}
private @NotNull Optional<Player> getTagger(OfflinePlayer player) {
return this.getFightTag(player)
.map(fightTagInter -> fightTagInter.getTagger())
.map(taggerId -> this.server.getPlayer(taggerId));
}
private Optional<FightTag> getFightTag(OfflinePlayer player) {
Player onlinePlayer = player.getPlayer();
if (onlinePlayer != null) {
if (!this.fightManager.isInCombat(onlinePlayer.getUniqueId())) {
return Optional.empty();
}
return Optional.of(this.fightManager.getTag(onlinePlayer.getUniqueId()));
}
return Optional.empty();
}
}
@@ -1,16 +1,20 @@
package com.eternalcode.combat.config;
import com.eternalcode.combat.notification.serializer.NotificationSerializer;
import com.eternalcode.multification.bukkit.notice.resolver.sound.SoundBukkitResolver;
import com.eternalcode.combat.border.animation.block.BlockTypeTransformer;
import com.eternalcode.combat.border.animation.particle.ParticleColorTransformer;
import com.eternalcode.combat.border.animation.particle.ParticleTypeTransformer;
import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults;
import com.eternalcode.multification.notice.resolver.NoticeResolverRegistry;
import com.eternalcode.multification.notice.resolver.sound.SoundAdventureResolver;
import com.eternalcode.multification.okaeri.MultificationSerdesPack;
import eu.okaeri.configs.ConfigManager;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.serdes.OkaeriSerdesPack;
import eu.okaeri.configs.serdes.SerdesRegistry;
import eu.okaeri.configs.serdes.commons.SerdesCommons;
import eu.okaeri.configs.yaml.bukkit.YamlBukkitConfigurer;
import eu.okaeri.configs.yaml.bukkit.serdes.SerdesBukkit;
import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
@@ -22,13 +26,16 @@ public class ConfigService {
public <T extends OkaeriConfig> T create(Class<T> config, File file) {
T configFile = ConfigManager.create(config);
YamlBukkitConfigurer yamlBukkitConfigurer = new YamlBukkitConfigurer();
YamlSnakeYamlConfigurer configurer = new YamlSnakeYamlConfigurer();
NoticeResolverRegistry noticeRegistry = NoticeResolverDefaults.createRegistry()
.registerResolver(new SoundBukkitResolver());
.registerResolver(new SoundAdventureResolver());
configFile.withConfigurer(yamlBukkitConfigurer, new SerdesCommons(), new SerdesBukkit());
configFile.withConfigurer(yamlBukkitConfigurer, new MultificationSerdesPack(noticeRegistry));
configFile.withSerdesPack(registry -> registry.register(new NotificationSerializer()));
configFile.withConfigurer(configurer,
new SerdesCommons(),
new SerdesBukkit(),
new MultificationSerdesPack(noticeRegistry),
new DefaultSerdesPack()
);
configFile.withBindFile(file);
configFile.withRemoveOrphans(true);
@@ -45,4 +52,13 @@ public class ConfigService {
config.load();
}
}
private static class DefaultSerdesPack implements OkaeriSerdesPack {
@Override
public void register(SerdesRegistry config) {
config.register(new ParticleColorTransformer());
config.register(new ParticleTypeTransformer());
config.register(new BlockTypeTransformer());
}
}
}
@@ -0,0 +1,20 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class AdminSettings extends OkaeriConfig {
@Comment({
"# Exclude server administrators from combat tagging and being tagged.",
"# Set to 'true' to prevent admins from being tagged or tagging others.",
"# Set to 'false' to allow admins to participate in combat."
})
public boolean excludeAdminsFromCombat = false;
@Comment({
"# Exclude players in creative mode from combat tagging and being tagged.",
"# Set to 'true' to prevent creative mode players from being tagged or tagging others.",
"# Set to 'false' to allow creative mode players to participate in combat."
})
public boolean excludeCreativePlayersFromCombat = false;
}
@@ -0,0 +1,43 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.util.List;
import org.bukkit.Material;
public class BlockPlacementSettings extends OkaeriConfig {
@Comment({
"# Prevent players from placing blocks during combat.",
"# Set to 'true' to block block placement while in combat."
})
public boolean disableBlockPlacing = true;
@Comment({
"# Restrict block placement above or below a specific Y coordinate.",
"# Available modes: ABOVE (blocks cannot be placed above the Y coordinate), BELOW (blocks cannot be placed below the Y coordinate)."
})
public BlockPlacingMode blockPlacementMode = BlockPlacingMode.ABOVE;
@Comment({
"# Custom name for the block placement mode used in messages.",
"# This name will be displayed in notifications related to block placement restrictions."
})
public String blockPlacementModeDisplayName = "above";
@Comment({
"# Define the Y coordinate for block placement restrictions.",
"# This value is relative to the selected block placement mode (ABOVE or BELOW)."
})
public int blockPlacementYCoordinate = 40;
@Comment({
"# Restrict the placement of specific blocks during combat.",
"# Add blocks to this list to prevent their placement. Leave the list empty to block all blocks.",
"# Note: This feature requires 'disableBlockPlacing' to be enabled."
})
public List<Material> restrictedBlockTypes = List.of();
public enum BlockPlacingMode {
ABOVE,
BELOW
}
}
@@ -0,0 +1,115 @@
package com.eternalcode.combat.config.implementation;
import com.eternalcode.combat.WhitelistBlacklistMode;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.util.List;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventPriority;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
public class CombatSettings extends OkaeriConfig {
@Comment({
"# Automatically release the attacker from combat when the victim dies.",
"# Set to 'true' to enable this feature, or 'false' to keep the attacker in combat."
})
public boolean releaseAttackerOnVictimDeath = true;
@Comment({
"# Disable the use of elytra during combat.",
"# Set to 'true' to prevent players from using elytra while in combat."
})
public boolean disableElytraUsage = true;
@Comment({
"# Disable the use of elytra when a player takes damage.",
"# Set to 'true' to disable elytra usage upon taking damage, even when the player is mid-air."
})
public boolean disableElytraOnDamage = true;
@Comment({
"# Prevent players from flying during combat.",
"# Flying players will fall to the ground if this is enabled."
})
public boolean disableFlying = true;
@Comment({
"# Forcefully unequip elytra when a player enters combat.",
"# This prevents players from abusing glide by jumping before being tagged.",
"# The elytra will be moved to the player's inventory.",
"# Recommended: true"
})
public boolean unequipElytraOnCombat = true;
@Comment({
"# Prevent players from boosting themselves while flying with fireworks",
"# This setting blocks usage of fireworks to boost elytra flight during combat"
})
public boolean disableFireworks = true;
@Comment({
"# Enable or disable combat logging for damage caused by non-player entities.",
"# Set to 'true' to log damage from non-player sources, or 'false' to disable this feature."
})
public boolean enableDamageCauseLogging = false;
@Comment({
"# Set the mode for logging damage causes.",
"# Available modes: WHITELIST (only listed causes are logged), BLACKLIST (all causes except listed ones are logged)."
})
public WhitelistBlacklistMode damageCauseRestrictionMode = WhitelistBlacklistMode.WHITELIST;
@Comment({
"# List of damage causes to be logged based on the selected mode.",
"# In WHITELIST mode, only these causes are logged. In BLACKLIST mode, all causes except these are logged.",
"# For a full list of damage causes, visit: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html"
})
public List<DamageCause> loggedDamageCauses = List.of(
DamageCause.LAVA,
DamageCause.CONTACT,
DamageCause.FIRE,
DamageCause.FIRE_TICK
);
@Comment({
"# List of projectile types that do not trigger combat tagging.",
"# Players hit by these projectiles will not be tagged as in combat.",
"# For a full list of entity types, visit: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/EntityType.html"
})
public List<EntityType> ignoredProjectileTypes = List.of(
EntityType.ENDER_PEARL,
EntityType.EGG
);
@Comment({
"# The event priority at which quit punishments should be handled.",
"# This determines when the plugin processes combat log punishment during PlayerQuitEvent.",
"# Options: LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR",
"# Tip: Set to LOWEST or LOW if you want quit punishments to happen before most other plugins.",
"# Default: NORMAL"
})
public EventPriority quitPunishmentEventPriority = EventPriority.NORMAL;
@Comment({
"# Keep a player's combat tag continuously active while they are below 'voidCombatHeight'.",
"# This prevents players from evading combat by falling into the void (e.g. in the End).",
"# Set to 'true' to keep resetting the combat timer to its full duration while below that height."
})
public boolean keepCombatActiveInVoid = false;
@Comment({
"# The Y height below which the combat timer is kept active (see 'keepCombatActiveInVoid').",
"# Default: 0, matching the void threshold in worlds such as the End."
})
public double voidCombatHeight = 0;
@Comment({
"# List of kick reasons where players will NOT be punished for combat logging.",
"# If this list is empty, players are ALWAYS punished when kicked during combat.",
"# If one of the listed phrases is found in the kick reason (case-insensitive),",
"# the player will NOT be punished.",
"# Example: 'Timed out', 'Kicked for inactivity', etc.",
"# To always punish players on kick, set: whitelistedKickReasons: []"
})
public List<String> whitelistedKickReasons = List.of("Kicked for inactivity", "Timed out", "Server is restarting");
}
@@ -0,0 +1,26 @@
package com.eternalcode.combat.config.implementation;
import com.eternalcode.combat.WhitelistBlacklistMode;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.util.List;
public class CommandSettings extends OkaeriConfig {
@Comment({
"# Set the mode for command restrictions during combat.",
"# Available modes: WHITELIST (only listed commands are allowed), BLACKLIST (listed commands are blocked)."
})
public WhitelistBlacklistMode commandRestrictionMode = WhitelistBlacklistMode.BLACKLIST;
@Comment({
"# List of commands affected by the command restriction mode.",
"# In BLACKLIST mode, these commands are blocked. In WHITELIST mode, only these commands are allowed."
})
public List<String> restrictedCommands = List.of(
"gamemode",
"spawn",
"tp",
"tpa",
"tpaccept"
);
}
@@ -0,0 +1,14 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class CrystalPvpSettings extends OkaeriConfig {
@Comment({"# Should player be tagged when damaged from crystal explosion set by other player"})
public boolean tagFromCrystals = true;
@Comment({"#Should player be tagged when damaged from respawn anchor explosion set by other player"})
public boolean tagFromRespawnAnchor = true;
}
@@ -0,0 +1,38 @@
package com.eternalcode.combat.config.implementation;
public enum InventoryAccessMode {
/**
* Allow access to all inventories during combat
*/
ALLOW_ALL,
/**
* Block access to all inventories during combat
*/
BLOCK_ALL,
/**
* Only block inventories in the restricted list
*/
BLACKLIST,
/**
* Only allow inventories in the allowed list
*/
WHITELIST;
/**
* Determines if an inventory type should be blocked based on the mode and whether it's in the list
*
* @param isInList whether the inventory type is in the configured list
* @return true if the inventory should be blocked
*/
public boolean shouldBlock(boolean isInList) {
return switch (this) {
case ALLOW_ALL -> false;
case BLOCK_ALL -> true;
case BLACKLIST -> isInList;
case WHITELIST -> !isInList;
};
}
}
@@ -0,0 +1,38 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import org.bukkit.event.inventory.InventoryType;
import java.util.List;
public class InventorySettings extends OkaeriConfig {
@Comment({
"# Inventory access restriction mode during combat",
"# ALLOW_ALL - Players can open all inventories during combat",
"# BLOCK_ALL - Players cannot open any inventories during combat",
"# BLACKLIST - Block only inventories listed in restrictedInventoryTypes",
"# WHITELIST - Allow only inventories listed in restrictedInventoryTypes"
})
public InventoryAccessMode inventoryAccessMode = InventoryAccessMode.ALLOW_ALL;
@Comment({
"# List of inventory types to restrict or allow (depending on the mode)",
"# In BLACKLIST mode: these inventories will be blocked",
"# In WHITELIST mode: only these inventories will be allowed",
"# Available types: CHEST, ENDER_CHEST, DISPENSER, DROPPER, FURNACE, WORKBENCH,",
"# CRAFTING, ENCHANTING, BREWING, MERCHANT, BEACON, ANVIL, SMITHING, HOPPER,",
"# SHULKER_BOX, BARREL, BLAST_FURNACE, LECTERN, SMOKER, LOOM, CARTOGRAPHY,",
"# GRINDSTONE, STONECUTTER, COMPOSTER, CHISELED_BOOKSHELF, JUKEBOX, SMITHING_NEW,",
"# CRAFTER, DECORATED_POT",
"# Note: PLAYER and CREATIVE inventories are never blocked to prevent game-breaking issues",
"# Full list: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/inventory/InventoryType.html"
})
public List<InventoryType> restrictedInventoryTypes = List.of(
InventoryType.CHEST,
InventoryType.ENDER_CHEST,
InventoryType.BARREL,
InventoryType.SHULKER_BOX
);
}
@@ -0,0 +1,233 @@
package com.eternalcode.combat.config.implementation;
import com.eternalcode.multification.notice.Notice;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class MessagesSettings extends OkaeriConfig {
@Comment({
"# Customize messages related to admin commands and notifications.",
"# These messages are displayed to server administrators."
})
public AdminMessages admin = new AdminMessages();
@Comment({
" ",
"# Configure the combat log notification displayed to players.",
"# You can use the {TIME} variable to display the remaining combat time.",
"# Placeholder {OPPONENT} will be replaced with player name or 'unknownPlayerPlaceholder' variable",
" ",
})
public Notice combatNotification = Notice.builder()
.actionBar("Combat ends in: <red>{TIME}</red>")
.build();
@Comment({
"# Would you like to display milliseconds instead of seconds in combat notification "
})
public boolean withoutMillis = true;
@Comment({
" ",
"# When player is not known this text will be used as {OPPONENT} in 'combatNotification'"
})
public String unknownPlayerPlaceholder = "Unknown";
@Comment({
"# Message displayed when a player lacks permission to execute a command.",
"# The {PERMISSION} placeholder is replaced with the required permission."
})
public Notice noPermission = Notice.chat(
"<gradient:red:dark_red>You don't have permission <dark_gray>(<gray>{PERMISSION}</gray>)</dark_gray> to perform this command!</gradient>");
@Comment({
"# Message displayed when a specified player is not found.",
"# This message is shown when a command targets a player who is not online or does not exist."
})
public Notice playerNotFound = Notice
.chat("<gradient:#ff0000:#ff6b6b>The specified player could not be found!</gradient>");
@Comment({
"# Message displayed when a player enters combat.",
"# This message warns the player not to leave the server while in combat."
})
public Notice playerTagged = Notice.chat(
"<gradient:red:yellow>⚠ <white>You are in combat!</white> <u>Do not leave the server!</gradient>");
@Comment({
"# Message displayed when a player leaves combat.",
"# This message informs the player that they can safely leave the server."
})
public Notice playerUntagged = Notice.chat(
"<gradient:#00ff00:#00b300>✌ <white>Combat ended!</white> You can now safely leave!</gradient>");
@Comment({
"# Broadcast message displayed when a player logs out during combat.",
"# The {PLAYER} placeholder is replaced with the player's name."
})
public Notice playerLoggedOutDuringCombat = Notice
.chat("<gradient:red:dark_red>⚠ <white>{PLAYER}</white> logged off during combat!</gradient>");
@Comment({
"# Message displayed when a player attempts to use a disabled command during combat.",
"# This message informs the player that the command is prohibited while in combat."
})
public Notice commandDisabledDuringCombat = Notice.chat(
"<gradient:red:yellow>⚠ <white>Command blocked!</white> Cannot use this during combat!</gradient>");
@Comment({
"# Message displayed when a player attempts to use an elytra during combat.",
"# This includes gliding, equipping, or having it forcefully removed.",
"# Informs the player that elytra usage is disabled in combat."
})
public Notice elytraDisabledDuringCombat = Notice.chat(
"<gradient:red:yellow>⚠ <white>Elytra disabled!</white> Cannot use elytra during combat!</gradient>"
);
@Comment({
"# Message displayed when a player uses a command with incorrect arguments.",
"# The {USAGE} placeholder is replaced with the correct command syntax."
})
public Notice invalidCommandUsage = Notice.chat("<gray>Usage: <gradient:gold:yellow>{USAGE}</gradient>");
@Comment({
"# Message displayed when a player attempts to open their inventory during combat.",
"# This message informs the player that inventory access is blocked while in combat."
})
public Notice inventoryBlockedDuringCombat = Notice.chat(
"<gradient:red:dark_red>⚠ <white>Inventory access</white> is restricted during combat!</gradient>");
@Comment({
"# Message displayed when a player attempts to place a block during combat.",
"# The {MODE} placeholder is replaced with the block placement mode (ABOVE/BELOW).",
"# The {Y} placeholder is replaced with the Y coordinate set in the config."
})
public Notice blockPlacingBlockedDuringCombat = Notice
.chat("<gradient:red:yellow>⚠ Block placement <white>{MODE} Y:{Y}</white> is restricted!</gradient>");
@Comment({
"# Message sent to the player using fireworks when fireworks during elytra flight are disabled"
})
public Notice fireworksDisabled = Notice
.actionbar("<gradient:red:yellow>⚠ You can't use fireworks during combat ⚠</gradient>");
@Comment({
"# Message displayed when a player attempts to enter a restricted region during combat.",
"# This message informs the player that they cannot enter the region while in combat."
})
public Notice cantEnterOnRegion = Notice.chat(
"<gradient:red:dark_red>⚠ Restricted area!</gradient> <white>Cannot enter during combat!</white>");
public static class AdminMessages extends OkaeriConfig {
@Comment({
"# Message displayed when the console attempts to use a player-only command.",
"# This message informs the console that the command is not available for non-players."
})
public Notice onlyForPlayers = Notice.chat("<gradient:red:dark_red>❌ This command is player-only!</gradient>");
@Comment({
"# Message displayed to an admin when they tag a player.",
"# The {PLAYER} placeholder is replaced with the tagged player's name."
})
public Notice adminTagPlayer = Notice
.chat("<gradient:#00b3ff:#0066ff>⚔ <gray>Tagged player:</gray> <white>{PLAYER}</white></gradient>");
@Comment({
"# Message displayed when an admin tags multiple players.",
"# The {FIRST_PLAYER} and {SECOND_PLAYER} placeholders are replaced with the players' names."
})
public Notice adminTagMultiplePlayers = Notice.chat(
"<gradient:#00b3ff:#0066ff>⚔ <gray>Tagged:</gray> <white>{FIRST_PLAYER}</white> <gray>and</gray> <white>{SECOND_PLAYER}</white></gradient>");
@Comment({
"# Message displayed to an admin when they remove a player from combat.",
"# The {PLAYER} placeholder is replaced with the player's name."
})
public Notice adminUntagPlayer = Notice.chat(
"<gradient:#00ff88:#00b300>✌ <gray>Removed</gray> <white>{PLAYER}</white> <gray>from combat</gray></gradient>");
@Comment({
"# Message displayed to an admin when they remove a player from combat.",
"# The {PLAYER} placeholder is replaced with the player's name."
})
public Notice adminUntagAll = Notice.chat(
"<gradient:#00ff88:#00b300>✌ <gray>Removed</gray> <white>{COUNT}</white> <gray> players from combat</gray></gradient>");
@Comment({
"# Message displayed when an admin attempts to untag a player who is not in combat.",
"# This message informs the admin that the player is not currently in combat."
})
public Notice adminPlayerNotInCombat = Notice
.chat("<gradient:red:dark_red>❌ <white>{PLAYER}</white> is not in combat!</gradient>");
@Comment({
"# Message displayed when a player is in combat.",
"# The {PLAYER} placeholder is replaced with the player's name."
})
public Notice playerInCombat = Notice
.chat("<gradient:#ff6666:#ff0000>⚔ <white>{PLAYER}</white> <gray>is in combat!</gray></gradient>");
@Comment({
"# Message displayed when a player is not in combat.",
"# The {PLAYER} placeholder is replaced with the player's name."
})
public Notice playerNotInCombat = Notice
.chat("<gradient:#00ff00:#00b300>✌ <white>{PLAYER}</white> <gray>is safe</gray></gradient>");
@Comment({
"# Message displayed when an admin attempts to tag themselves.",
"# This message informs the admin that they cannot tag themselves."
})
public Notice adminCannotTagSelf = Notice.chat("<gradient:red:dark_red>❌ Cannot tag yourself!</gradient>");
@Comment({
"# Message displayed when an admin disables combat tagging for themselves.",
"# The {TIME} placeholder is replaced with the remaining fight time."
})
public Notice adminTagOutSelf = Notice.chat(
"<gradient:#00b3ff:#0066ff>🛡 <gray>Self-protection active for</gray> <white>{TIME}</white></gradient>");
@Comment({
"# Message displayed when an admin disables combat tagging for another player.",
"# The {PLAYER} placeholder is replaced with the player's name.",
"# The {TIME} placeholder is replaced with the remaining fight time."
})
public Notice adminTagOut = Notice.chat(
"<gradient:#00b3ff:#0066ff>🛡 <gray>Protected</gray> <white>{PLAYER}</white> <gray>for</gray> <white>{TIME}</white></gradient>");
@Comment({
"# Message displayed to a player when their combat tagging is disabled.",
"# The {TIME} placeholder is replaced with the remaining fight time."
})
public Notice playerTagOut = Notice.chat(
"<gradient:#00ff88:#00b300>🛡 <gray>Protection active for</gray> <white>{TIME}</white></gradient>");
@Comment({
"# Message displayed when an admin reenables combat tagging for a player.",
"# The {PLAYER} placeholder is replaced with the player's name."
})
public Notice adminTagOutOff = Notice.chat(
"<gradient:#00ff88:#00b300>✌ <gray>Re-enabled tagging for</gray> <white>{PLAYER}</white></gradient>");
@Comment({
"# Message displayed to a player when their combat tagging is reenabled.",
"# This message informs the player that they can now be tagged again."
})
public Notice playerTagOutOff = Notice.chat("");
@Comment({
"# Message displayed when an admin attempts to tag a player who has tag-out enabled.",
"# This message informs the admin that the player cannot be tagged at this time."
})
public Notice adminTagOutCanceled = Notice
.chat("<gradient:red:dark_red>❌ Player has tag-out protection!</gradient>");
@Comment({
"# Message displayed when an admin checks combat statistics.",
"# The {COUNT} placeholder is replaced with the number of players currently in combat."
})
public Notice combatStats = Notice
.chat("<gradient:#ff6666:#ff0000>⚔ <gray>Players in combat:</gray> <white>{COUNT}</white></gradient>");
}
}
@@ -0,0 +1,14 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class PlaceholderSettings extends OkaeriConfig {
@Comment("Text returned by %eternalcombat_isInCombat_formatted% placeholder when the player is in combat")
public String isInCombatFormattedTrue = "In Combat";
@Comment("Text returned by %eternalcombat_isInCombat_formatted% placeholder when the player is out of combat")
public String isInCombatFormattedFalse = "Not In Combat";
}
@@ -1,263 +1,172 @@
package com.eternalcode.combat.config.implementation;
import com.eternalcode.combat.WhitelistBlacklistMode;
import com.eternalcode.combat.border.BorderSettings;
import com.eternalcode.combat.fight.death.DeathSettings;
import com.eternalcode.combat.fight.drop.DropSettings;
import com.eternalcode.combat.fight.effect.FightEffectSettings;
import com.eternalcode.combat.fight.pearl.FightPearlSettings;
import com.eternalcode.multification.bukkit.notice.BukkitNotice;
import com.eternalcode.multification.notice.Notice;
import com.eternalcode.combat.fight.knockback.KnockbackSettings;
import com.eternalcode.combat.fight.pearl.PearlSettings;
import com.eternalcode.combat.fight.trident.TridentSettings;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.EntityDamageEvent;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
public class PluginConfig extends OkaeriConfig {
@Comment("# Do you want to change the plugin settings?")
@Comment(" ")
@Comment("# _____ _ EternalCombat _ _____ _ _ ")
@Comment("# | ___| | o()xxx[{:::::::::> | / __ \\ | | | | ")
@Comment("# | |__ | |_ ___ _ __ _ __ __ _| | / \\/ ___ _ __ ___ | |__ __ _| |_ ")
@Comment("# | __|| __/ _ \\ '__| '_ \\ / _` | | | / _ \\| '_ ` _ \\| '_ \\ / _` | __| ")
@Comment("# | |___| || __/ | | | | | (_| | | \\__/\\ (_) | | | | | | |_) | (_| | |_ ")
@Comment("# \\____/ \\__\\___|_| |_| |_|\\__,_|_|\\____/\\___/|_| |_| |_|_.__/ \\__,_|\\__| ")
@Comment(" ")
@Comment({
" ",
"# Settings for the plugin.",
"# Modify these to customize the plugin's behavior."
})
public Settings settings = new Settings();
@Comment({ " ", "# Ender pearl settings" })
public FightPearlSettings pearl = new FightPearlSettings();
@Comment({
" ",
"# Settings related to Ender Pearls.",
"# Configure cooldowns, restrictions, and other behaviors for Ender Pearls during combat."
})
public PearlSettings pearl = new PearlSettings();
@Comment({ " ", "# Custom effects settings" })
@Comment({
" ",
"# Settings related to Trident",
"# Configure cooldowns, restrictions, and other behaviors for Trident during combat."
})
public TridentSettings trident = new TridentSettings();
@Comment({
" ",
"# Custom effects applied during combat.",
"# Configure effects like blindness, slowness, or other debuffs that are applied to players in combat."
})
public FightEffectSettings effect = new FightEffectSettings();
@Comment({ " ", "# Set a custom way for a player's items to drop on death (if in combat)" })
public DropSettings dropSettings = new DropSettings();
@Comment({
" ",
"# This section contains effects displayed on death of the player"
})
public DeathSettings death = new DeathSettings();
@Comment({
" ",
"# Customize how items are dropped when a player dies during combat.",
"# Configure whether items drop, how they drop, and any additional rules for item drops."
})
public DropSettings drop = new DropSettings();
@Comment({
" ",
"# Settings related to knockback during combat.",
"# Configure knockback settings and behaviors for players in combat."
})
public KnockbackSettings knockback = new KnockbackSettings();
@Comment({
" ",
"# Border Settings",
"# Configure the border that appears during combat.",
})
public BorderSettings border = new BorderSettings();
@Comment({
" ",
"# Settings related to block placement during combat.",
"# Configure restrictions and behaviors for block placement while players are in combat."
})
public BlockPlacementSettings blockPlacement = new BlockPlacementSettings();
@Comment({
" ",
"# Settings related to crystal PvP.",
"# Configure behaviors, restrictions, and features specific to crystal PvP combat."
})
public CrystalPvpSettings crystalPvp = new CrystalPvpSettings();
@Comment({
" ",
"# Settings related to commands during combat.",
"# Configure command restrictions and behaviors for players in combat."
})
public CommandSettings commands = new CommandSettings();
@Comment({
" ",
"# Settings related to the plugin's admin commands and features.",
"# Configure admin-specific settings and behaviors for the plugin."
})
public AdminSettings admin = new AdminSettings();
@Comment({
" ",
"# Settings related to regions.",
"# Configure region-specific settings and behaviors for combat."
})
public RegionSettings regions = new RegionSettings();
@Comment({
" ",
"# Settings related to combat and player tagging.",
"# Configure combat rules, and behaviors for player tagging."
})
public CombatSettings combat = new CombatSettings();
@Comment({
" ",
"# Settings related to inventory access during combat.",
"# Configure which inventories players can or cannot access while in combat."
})
public InventorySettings inventory = new InventorySettings();
@Comment({
" ",
"# Settings related to sign editing during combat.",
"# Created for sign traps where opening the sign editor can block ender pearl throws."
})
public SignEditingSettings signEditing = new SignEditingSettings();
@Comment({
" ",
"# Settings related to placeholders used in the plugin.",
"# Configure default values returned by placeholders"
})
public PlaceholderSettings placeholders = new PlaceholderSettings();
@Comment({
" ",
"# Customize the messages displayed by the plugin.",
"# Modify these to change the text and formatting of notifications and alerts."
})
public MessagesSettings messagesSettings = new MessagesSettings();
public static class Settings extends OkaeriConfig {
@Comment({
"# Notify players about new plugin updates when they join the server.",
"# Set to 'true' to enable update notifications, or 'false' to disable them."
})
public boolean notifyAboutUpdates = true;
@Comment("# Whether the player should receive information about new plugin updates upon joining the server")
public boolean shouldReceivePluginUpdates = true;
@Comment({
"# The duration (in seconds) that a player remains in combat after being attacked.",
"# After this time expires, the player will no longer be considered in combat."
})
public Duration combatTimerDuration = Duration.ofSeconds(20);
@Comment("# The duration of combat in seconds")
public Duration combatDuration = Duration.ofSeconds(20);
@Comment("# List of worlds to ignore")
public List<String> worldsToIgnore = List.of(
@Comment({
"# List of worlds where combat logging is disabled.",
"# Players in these worlds will not be tagged or affected by combat rules."
})
public List<String> ignoredWorlds = List.of(
"your_world"
);
@Comment("# List of regions to block")
public List<String> blockedRegions = Collections.singletonList("your_region");
@Comment({
"# Set the knock multiplier for the blocked region",
"# Values can be decimal. Do NOT use negative values.",
"# Setting it around 1 knocks the player around 2-4 blocks away."
})
public double blockedRegionKnockMultiplier = 1;
@Comment({ "# Should the player be prevented from entering regions with WorldGuard flag PVP set to DENY " })
public boolean shouldPreventPvpRegions = true;
@Comment("# Set the radius of the blocked region if you aren't using WorldGuard basen on default spawn region!")
public int blockedRegionRadius = 10;
@Comment("# Release attacker after victim dies?")
public boolean shouldReleaseAttacker = true;
@Comment({ "# If you want to exclude admins from combat, ",
"# Setting this to true - admins cannot be tagged and will not tag other players on hit",
"# Setting this to false - admins can be tagged and can tag other players on hit"
})
public boolean excludeOpFromCombat = false;
@Comment({"# If you want to exclude players in creative mode from combat, ",
"# Setting this to true - players in creative mode cannot be tagged and will not tag other players on hit",
"# Setting this to false - players in creative mode can be tag others"
})
public boolean excludeCreativeFromCombat = false;
@Comment("# Command blocking mode, available modes: WHITELIST, BLACKLIST")
public WhitelistBlacklistMode commandBlockingMode = WhitelistBlacklistMode.BLACKLIST;
@Comment({
"# List of commands based on the mode above",
"# Based on BLACKLIST mode, all commands in the list are blocked, and all others are allowed",
"# Based on WHITELIST mode, all commands in the list are allowed, and all others are blocked",
})
public List<String> blockedCommands = List.of(
"gamemode",
"spawn",
"tp"
);
@Comment("# Block the use of elytra?")
public boolean shouldPreventElytraUsage = true;
@Comment("# Block flying? (flying players will fall to the ground)")
public boolean shouldPreventFlying = true;
@Comment("# Disable the use of elytra on damage?")
public boolean shouldElytraDisableOnDamage = true;
@Comment("# Block the opening of inventory?")
public boolean shouldPreventInventoryOpening = true;
@Comment("# Whether to block the placement of blocks?")
public boolean shouldPreventBlockPlacing = true;
@Comment({ "# Block the placement of blocks above or below a certain Y coordinate",
"# Select the mode for block placing, available modes: ABOVE, BELOW"
})
public BlockPlacingMode blockPlacingMode = BlockPlacingMode.ABOVE;
@Comment("# Block placing mode custom name used if messages")
public String blockPlacingModeName = "above";
@Comment("# Set the Y coordinate for block placing relative to mode selected above")
public int blockPlacingYCoordinate = 40;
public enum BlockPlacingMode {
ABOVE,
BELOW
}
@Comment({
"# Disable placing specific blocks?",
"# If you want to block all blocks, enable shouldPreventBlockPlacing and make this list empty",
"# If you want to disable placing only specific blocks, enable shouldPreventBlockPlacing and add blocks to this list above",
"# If you want to disable this feature completely, disable shouldPreventBlockPlacing option above",
})
public List<Material> specificBlocksToPreventPlacing = List.of();
@Comment("# Do You want to enable combat log for non-player causes of damage? - Set to false to disable")
public boolean shouldEnableDamageCauses = false;
@Comment("# Select the mode for damage causes, available modes: WHITELIST, BLACKLIST")
public WhitelistBlacklistMode damageCausesMode = WhitelistBlacklistMode.WHITELIST;
@Comment({
"# After selecting the mode above, select the causes of damage to be logged",
"# You can find a list of all causes here: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html",
"# While using Whitelist mode the player will get a combat log after the damage from the list below",
"# While using Blacklist mode the player will get a combat log after any damage non-listed below",
})
public List<EntityDamageEvent.DamageCause> damageCausesToLog = List.of(
EntityDamageEvent.DamageCause.LAVA,
EntityDamageEvent.DamageCause.CONTACT,
EntityDamageEvent.DamageCause.FIRE,
EntityDamageEvent.DamageCause.FIRE_TICK
);
@Comment({
"# After what type of projectile entity should not tag the player as fighter?",
"# You can find a list of all entity types here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/EntityType.html"
})
public List<EntityType> disabledProjectileEntities = List.of(
EntityType.ENDER_PEARL
);
}
@Comment({ " ", "# Do you want to change the plugin messages?" })
public Messages messages = new Messages();
public static class Messages extends OkaeriConfig {
@Comment("# Do you want to change the admin messages?")
public AdminMessages admin = new AdminMessages();
@Comment({
" ",
"# Combat log notification",
"# You can use {TIME} variable to display the time left in combat",
"# Notification types: CHAT, ACTION_BAR, TITLE, SUB_TITLE, BOSS_BAR",
" ",
"# BossBar progress: This is the value of the progress bar. Set it to -1.0 to show the remaining combat time",
"# BossBar colors: https://javadoc.io/static/net.kyori/adventure-api/4.14.0/net/kyori/adventure/bossbar/BossBar.Color.html",
"# BossBar overlays: https://javadoc.io/static/net.kyori/adventure-api/4.14.0/net/kyori/adventure/bossbar/BossBar.Overlay.html"
})
public Notice combatNotification = BukkitNotice.builder()
.actionBar("&dCombat ends in: &f{TIME}")
.sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.PLAYERS, 2.0F, 1.0F)
.build();
@Comment("# Message sent when the player does not have permission to perform a command")
public Notice noPermission = Notice.chat("&cYou don't have permission \"{PERMISSION}\" to perform this command!");
@Comment("# Message sent when the specified player could not be found")
public Notice playerNotFound = Notice.chat("&cThe specified player could not be found!");
@Comment("# Message sent when the player enters combat")
public Notice playerTagged = Notice.chat("&cYou are in combat, do not leave the server!");
@Comment("# Message sent when the player leaves combat")
public Notice playerUntagged = Notice.chat("&aYou are no longer in combat! You can safely leave the server.");
@Comment("# This is broadcast when the player is in combat and logs out")
public Notice playerLoggedOutDuringCombat = Notice.chat("&c{PLAYER} logged off during the fight!");
@Comment({
"# Message sent when the player is in combat and tries to use a disabled command",
"# you can configure the list of disabled commands in the blockedCommands section of the config.yml file"
})
public Notice commandDisabledDuringCombat = Notice.chat("&cUsing this command during combat is prohibited!");
@Comment("# Message sent when player tries to use a command with invalid arguments")
public Notice invalidCommandUsage = Notice.chat("&7Correct usage: &e{USAGE}");
@Comment("# Message sent when player tries to open inventory, but the inventory open is blocked")
public Notice inventoryBlockedDuringCombat = Notice.chat("&cYou cannot open this inventory during combat!");
@Comment({ "# Message sent when player tries to place a block, but the block place is blocked",
"# Placeholder {Y} is replaced with the Y coordinate set in the config",
"# Placeholder {MODE} is replaced with the mode set in the config" })
public Notice blockPlacingBlockedDuringCombat = Notice.chat("&cYou cannot place {MODE} {Y} coordinate during combat!");
@Comment("# Message sent when player tries to enter a region")
public Notice cantEnterOnRegion = Notice.chat("&cYou can't enter this region during combat!");
public static class AdminMessages extends OkaeriConfig {
@Comment("# Message sent when console tries to use a command that is only for players")
public Notice onlyForPlayers = Notice.chat("&cThis command is only available to players!");
@Comment("# Message sent to admin when they tag a player")
public Notice adminTagPlayer = Notice.chat("&7You have tagged &e{PLAYER}");
@Comment("# Message sent when a player is tagged by an admin")
public Notice adminTagMultiplePlayers = Notice.chat("&7You have tagged &e{FIRST_PLAYER}&7 and &e{SECOND_PLAYER}&7.");
@Comment("# Message sent to admin when they remove a player from combat")
public Notice adminUntagPlayer = Notice.chat("&7You have removed &e{PLAYER}&7 from the fight.");
@Comment("# Message sent when the player is not in combat")
public Notice adminPlayerNotInCombat = Notice.chat("&cThis player is not in combat!");
@Comment("# Message sent when the player is in combat")
public Notice playerInCombat = Notice.chat("&c{PLAYER} is currently in combat!");
@Comment("# Message sent when a player is not in combat")
public Notice playerNotInCombat = Notice.chat("&a{PLAYER} is not currently in combat.");
@Comment("# Message sent when an admin tries to tag themselves")
public Notice adminCannotTagSelf = Notice.chat("&cYou cannot tag yourself!");
@Comment("# Message sent when an admin disables the ability to get tagged for some time")
public Notice adminTagOutSelf = Notice.chat("&7Successfully disabled tag for Yourself! You will be taggable after &e{TIME} ");
@Comment("# Message sent when an admin disables the ability to get tagged for some time for other player")
public Notice adminTagOut = Notice.chat("&7Successfully disabled tag for &e{PLAYER}! They will be taggable after &e{TIME} ");
@Comment("# Message sent to the player whom the ability to get tagged for some time has been disabled")
public Notice playerTagOut = Notice.chat("&7You will be taggable in &e{TIME} !");
@Comment("# Message sent when an admin reenables the ability to get tagged for the player")
public Notice adminTagOutOff = Notice.chat("&7Successfully enabled tag for &e{PLAYER}!");
@Comment("# Message sent to the player whom the ability to get tagged has been reenabled")
public Notice playerTagOutOff = Notice.chat("&7You are now taggable!");
@Comment("# Message sent when player cannot be tagged because they have enabled tag-out")
public Notice adminTagOutCanceled = Notice.chat("&cCannot tag this player due to tag-out!");
}
}
}
@@ -0,0 +1,26 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.util.Collections;
import java.util.List;
public class RegionSettings extends OkaeriConfig {
@Comment({
"# List of regions where combat is restricted.",
"# Players in these regions will not be able to engage in combat."
})
public List<String> blockedRegions = Collections.singletonList("your_region");
@Comment({
"# Prevent players from entering regions where PVP is disabled by WorldGuard.",
"# Set to 'true' to enforce this restriction, or 'false' to allow PVP in all regions."
})
public boolean preventPvpInRegions = true;
@Comment({
"# Define the radius of restricted regions if WorldGuard is not used.",
"# This setting is based on the default spawn region."
})
public int restrictedRegionRadius = 10;
}
@@ -0,0 +1,13 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class SignEditingSettings extends OkaeriConfig {
@Comment({
"# Prevent players from editing signs during combat.",
"# Disabled by default; created for sign traps where sign editing blocks ender pearl throws."
})
public boolean disableSignEditingDuringCombat = false;
}
@@ -0,0 +1,33 @@
package com.eternalcode.combat.crystalpvp;
import java.util.Optional;
import java.util.UUID;
import org.bukkit.metadata.MetadataValueAdapter;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class CrystalMetadata extends MetadataValueAdapter {
private UUID damager;
protected CrystalMetadata(@NotNull Plugin owningPlugin, UUID damager) {
super(owningPlugin);
this.damager = damager;
}
Optional<UUID> getDamager() {
return Optional.ofNullable(this.damager);
}
@Override
public @Nullable UUID value() {
return this.damager;
}
@Override
public void invalidate() {
this.damager = null;
}
}
@@ -0,0 +1,79 @@
package com.eternalcode.combat.crystalpvp;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.EnderCrystal;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByBlockEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.metadata.MetadataValue;
public class CrystalPvpConstants {
private CrystalPvpConstants() {
}
public static final String CRYSTAL_METADATA = "eternalcombat:crystal";
public static final String ANCHOR_METADATA = "eternalcombat:anchor";
public static Optional<UUID> getDamagerUniqueIdFromEndCrystal(EntityDamageByEntityEvent event) {
if (event.getDamager() instanceof EnderCrystal enderCrystal) {
List<MetadataValue> metadataValues = enderCrystal.getMetadata(CRYSTAL_METADATA);
return metadataValues
.stream()
.filter(source -> source instanceof CrystalMetadata)
.map(meta -> (CrystalMetadata) meta)
.findFirst()
.flatMap(CrystalMetadata::getDamager);
}
return Optional.empty();
}
public static Optional<UUID> getDamagerUniqueIdFromRespawnAnchor(EntityDamageByBlockEvent event) {
Block damager = event.getDamager();
if (damager == null) {
return Optional.empty();
}
if (damager.getType() != Material.RESPAWN_ANCHOR) {
return Optional.empty();
}
return damager.getMetadata(ANCHOR_METADATA).stream()
.filter(source -> source instanceof CrystalMetadata)
.map(meta -> (CrystalMetadata) meta)
.findFirst()
.flatMap(metadata -> metadata.getDamager());
}
static void handleCombatTag(
Optional<UUID> optionalDamagerUUID,
Player player,
FightManager fightManager,
PluginConfig pluginConfig
) {
UUID victimUniqueId = player.getUniqueId();
if (optionalDamagerUUID.isPresent()) {
UUID damagerUniqueId = optionalDamagerUUID.get();
if (!damagerUniqueId.equals(victimUniqueId)) {
fightManager.tag(
damagerUniqueId,
pluginConfig.settings.combatTimerDuration,
CauseOfTag.CRYSTAL
);
fightManager.tag(
victimUniqueId,
pluginConfig.settings.combatTimerDuration,
CauseOfTag.CRYSTAL
);
}
}
}
}
@@ -0,0 +1,70 @@
package com.eternalcode.combat.crystalpvp;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import java.util.Optional;
import java.util.UUID;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.EnderCrystal;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.plugin.Plugin;
import static com.eternalcode.combat.crystalpvp.CrystalPvpConstants.CRYSTAL_METADATA;
public class EndCrystalListener implements Listener {
private final Plugin plugin;
private final FightManager fightManager;
private final PluginConfig pluginConfig;
public EndCrystalListener(Plugin plugin, FightManager fightManager, PluginConfig pluginConfig) {
this.plugin = plugin;
this.fightManager = fightManager;
this.pluginConfig = pluginConfig;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onPlayerDamageCrystal(EntityDamageByEntityEvent event) {
if (event.getEntity() instanceof EnderCrystal enderCrystal) {
if (event.getDamager() instanceof Arrow arrow && arrow.getShooter() instanceof Player player) {
enderCrystal.setMetadata(CRYSTAL_METADATA, new CrystalMetadata(this.plugin, player.getUniqueId()));
}
if (!(event.getDamager() instanceof Player player)) {
return;
}
enderCrystal.setMetadata(CRYSTAL_METADATA, new CrystalMetadata(this.plugin, player.getUniqueId()));
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onDamage(EntityDamageByEntityEvent event) {
if (!this.pluginConfig.crystalPvp.tagFromCrystals) {
return;
}
if (pluginConfig.settings.ignoredWorlds.contains(event.getEntity().getWorld().getName())) {
return;
}
Optional<UUID> optionalDamagerUUID = CrystalPvpConstants.getDamagerUniqueIdFromEndCrystal(event);
if (optionalDamagerUUID.isEmpty()) {
return;
}
if (event.getEntity() instanceof Player player) {
CrystalPvpConstants.handleCombatTag(
optionalDamagerUUID,
player,
this.fightManager,
this.pluginConfig
);
}
}
}
@@ -0,0 +1,89 @@
package com.eternalcode.combat.crystalpvp;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import java.util.Optional;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.RespawnAnchor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.entity.EntityDamageByBlockEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import static com.eternalcode.combat.crystalpvp.CrystalPvpConstants.ANCHOR_METADATA;
public class RespawnAnchorListener implements Listener {
private final Plugin plugin;
private final FightManager fightManager;
private final PluginConfig pluginConfig;
public RespawnAnchorListener(Plugin plugin, FightManager fightManager, PluginConfig pluginConfig) {
this.plugin = plugin;
this.fightManager = fightManager;
this.pluginConfig = pluginConfig;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onAnchorInteract(PlayerInteractEvent event) {
Block block = event.getClickedBlock();
if (block == null || block.getType() != Material.RESPAWN_ANCHOR) {
return;
}
if (!(block.getBlockData() instanceof RespawnAnchor respawnAnchor)) {
return;
}
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
int charges = respawnAnchor.getCharges();
ItemStack item = event.getItem();
boolean isGlowstone = item != null && item.getType() == Material.GLOWSTONE;
if ((charges > 0 && !isGlowstone) || charges == respawnAnchor.getMaximumCharges()) {
addMetaData(event, block);
}
}
private void addMetaData(PlayerInteractEvent event, Block block) {
block.setMetadata(
ANCHOR_METADATA,
new CrystalMetadata(this.plugin, event.getPlayer().getUniqueId())
);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
void onAnchorExplosion(EntityDamageByBlockEvent event) {
if (!this.pluginConfig.crystalPvp.tagFromRespawnAnchor) {
return;
}
if (pluginConfig.settings.ignoredWorlds.contains(event.getEntity().getWorld().getName())) {
return;
}
if (!(event.getEntity() instanceof Player player)) {
return;
}
Optional<UUID> optionalDamagerUniqueId = CrystalPvpConstants.getDamagerUniqueIdFromRespawnAnchor(event);
if (optionalDamagerUniqueId.isEmpty()) {
return;
}
CrystalPvpConstants.handleCombatTag(optionalDamagerUniqueId, player, this.fightManager, this.pluginConfig);
}
}
@@ -0,0 +1,9 @@
package com.eternalcode.combat.event;
import org.bukkit.event.Listener;
public interface DynamicListener<E> extends Listener {
void onEvent(E event);
}
@@ -1,20 +0,0 @@
package com.eternalcode.combat.event;
import org.bukkit.Server;
import org.bukkit.event.Event;
public class EventCaller {
private final Server server;
public EventCaller(Server server) {
this.server = server;
}
public <T extends Event> T publishEvent(T event) {
this.server.getPluginManager().callEvent(event);
return event;
}
}
@@ -0,0 +1,37 @@
package com.eternalcode.combat.event;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
public class EventManager {
private final Plugin plugin;
public EventManager(Plugin plugin) {
this.plugin = plugin;
}
public <T extends Event> T publishEvent(T event) {
this.plugin.getServer().getPluginManager().callEvent(event);
return event;
}
public void subscribe(Listener... listeners) {
for (Listener listener : listeners) {
plugin.getServer().getPluginManager().registerEvents(listener, plugin);
}
}
public <E extends Event, L extends DynamicListener<E>> void subscribe(Class<E> type, EventPriority priority, L listener) {
plugin.getServer().getPluginManager().registerEvents(listener, plugin);
plugin.getServer().getPluginManager().registerEvent(type, listener, priority, (l, event) -> {
if (type.isInstance(event)) {
listener.onEvent(type.cast(event));
}
}, plugin);
}
}
@@ -1,6 +1,6 @@
package com.eternalcode.combat.fight;
import com.eternalcode.combat.event.EventCaller;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
@@ -19,10 +19,10 @@ import org.jetbrains.annotations.Nullable;
public class FightManagerImpl implements FightManager {
private final Map<UUID, FightTag> fights = new ConcurrentHashMap<>();
private final EventCaller eventCaller;
private final EventManager eventManager;
public FightManagerImpl(EventCaller eventCaller) {
this.eventCaller = eventCaller;
public FightManagerImpl(EventManager eventManager) {
this.eventManager = eventManager;
}
@Override
@@ -38,7 +38,7 @@ public class FightManagerImpl implements FightManager {
@Override
public FightUntagEvent untag(UUID player, CauseOfUnTag causeOfUnTag) {
FightUntagEvent event = this.eventCaller.publishEvent(new FightUntagEvent(player, causeOfUnTag));
FightUntagEvent event = this.eventManager.publishEvent(new FightUntagEvent(player, causeOfUnTag));
if (event.isCancelled()) {
return event;
}
@@ -55,7 +55,7 @@ public class FightManagerImpl implements FightManager {
@ApiStatus.Experimental
@Override
public FightTagEvent tag(UUID target, Duration delay, CauseOfTag causeOfTag, @Nullable UUID tagger) {
FightTagEvent event = this.eventCaller.publishEvent(new FightTagEvent(target, causeOfTag));
FightTagEvent event = this.eventManager.publishEvent(new FightTagEvent(target, causeOfTag));
if (event.isCancelled()) {
return event;
@@ -1,12 +1,13 @@
package com.eternalcode.combat.fight;
import com.eternalcode.combat.config.implementation.MessagesSettings;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CancelTagReason;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import com.eternalcode.combat.notification.NotificationAnnouncer;
import com.eternalcode.combat.notification.NoticeService;
import dev.rollczi.litecommands.annotations.argument.Arg;
import dev.rollczi.litecommands.annotations.command.Command;
import dev.rollczi.litecommands.annotations.context.Context;
@@ -16,6 +17,7 @@ import dev.rollczi.litecommands.annotations.priority.Priority;
import dev.rollczi.litecommands.annotations.priority.PriorityValue;
import java.time.Duration;
import java.util.UUID;
import java.util.stream.Collectors;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -23,12 +25,12 @@ import org.bukkit.entity.Player;
public class FightTagCommand {
private final FightManager fightManager;
private final NotificationAnnouncer announcer;
private final NoticeService noticeService;
private final PluginConfig config;
public FightTagCommand(FightManager fightManager, NotificationAnnouncer announcer, PluginConfig config) {
public FightTagCommand(FightManager fightManager, NoticeService noticeService, PluginConfig config) {
this.fightManager = fightManager;
this.announcer = announcer;
this.noticeService = noticeService;
this.config = config;
}
@@ -37,15 +39,14 @@ public class FightTagCommand {
void status(@Context CommandSender sender, @Arg Player target) {
UUID targetUniqueId = target.getUniqueId();
this.announcer.create()
this.noticeService.create()
.notice(this.fightManager.isInCombat(targetUniqueId)
? this.config.messages.admin.playerInCombat
: this.config.messages.admin.playerNotInCombat
? this.config.messagesSettings.admin.playerInCombat
: this.config.messagesSettings.admin.playerNotInCombat
)
.placeholder("{PLAYER}", target.getName())
.viewer(sender)
.send();
}
@Execute(name = "tag")
@@ -53,36 +54,35 @@ public class FightTagCommand {
@Priority(PriorityValue.HIGH)
void tag(@Context CommandSender sender, @Arg Player target) {
UUID targetUniqueId = target.getUniqueId();
Duration time = this.config.settings.combatDuration;
Duration time = this.config.settings.combatTimerDuration;
FightTagEvent event = this.fightManager.tag(targetUniqueId, time, CauseOfTag.COMMAND);
if (event.isCancelled()) {
CancelTagReason cancelReason = event.getCancelReason();
this.tagoutReasonHandler(sender, cancelReason, this.config.messages);
this.tagoutReasonHandler(sender, cancelReason, this.config.messagesSettings);
return;
}
this.announcer.create()
.notice(this.config.messages.admin.adminTagPlayer)
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminTagPlayer)
.placeholder("{PLAYER}", target.getName())
.viewer(sender)
.send();
}
@Execute(name = "tag")
@Permission("eternalcombat.tag")
void tagMultiple(@Context CommandSender sender, @Arg Player firstTarget, @Arg Player secondTarget) {
Duration combatTime = this.config.settings.combatDuration;
PluginConfig.Messages messages = this.config.messages;
Duration combatTime = this.config.settings.combatTimerDuration;
MessagesSettings messagesSettings = this.config.messagesSettings;
if (sender.equals(firstTarget) || sender.equals(secondTarget)) {
this.announcer.create()
.notice(messages.admin.adminCannotTagSelf)
this.noticeService.create()
.notice(messagesSettings.admin.adminCannotTagSelf)
.viewer(sender)
.send();
@@ -95,7 +95,7 @@ public class FightTagCommand {
if (firstTagEvent.isCancelled()) {
CancelTagReason cancelReason = firstTagEvent.getCancelReason();
this.tagoutReasonHandler(sender, cancelReason, messages);
this.tagoutReasonHandler(sender, cancelReason, messagesSettings);
return;
}
@@ -103,7 +103,7 @@ public class FightTagCommand {
if (secondTagEvent.isCancelled()) {
CancelTagReason cancelReason = secondTagEvent.getCancelReason();
this.tagoutReasonHandler(sender, cancelReason, messages);
this.tagoutReasonHandler(sender, cancelReason, messagesSettings);
return;
}
@@ -112,24 +112,24 @@ public class FightTagCommand {
return;
}
this.announcer.create()
.notice(messages.admin.adminTagMultiplePlayers)
this.noticeService.create()
.notice(messagesSettings.admin.adminTagMultiplePlayers)
.placeholder("{FIRST_PLAYER}", firstTarget.getName())
.placeholder("{SECOND_PLAYER}", secondTarget.getName())
.viewer(sender)
.send();
}
@Execute(name = "untag")
@Permission("eternalcombat.untag")
void untag(@Context Player sender, @Arg Player target) {
void untag(@Context CommandSender sender, @Arg Player target) {
UUID targetUniqueId = target.getUniqueId();
if (!this.fightManager.isInCombat(targetUniqueId)) {
this.announcer.create()
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminPlayerNotInCombat)
.placeholder("{PLAYER}", target.getName())
.viewer(sender)
.notice(this.config.messages.admin.adminPlayerNotInCombat)
.send();
return;
}
@@ -139,21 +139,52 @@ public class FightTagCommand {
return;
}
this.announcer.create()
.notice(this.config.messages.admin.adminUntagPlayer)
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminUntagPlayer)
.placeholder("{PLAYER}", target.getName())
.viewer(sender)
.send();
}
private void tagoutReasonHandler(CommandSender sender, CancelTagReason cancelReason, PluginConfig.Messages messages) {
@Execute(name = "untagall")
@Permission("eternalcombat.untagall")
void untagAll(@Context CommandSender sender) {
int combatPlayersSize = this.fightManager.getFights().size();
this.fightManager.getFights().stream()
.map(FightTag::getTaggedPlayer)
.collect(Collectors.toSet())
.forEach(uuid -> this.fightManager.untag(uuid, CauseOfUnTag.COMMAND));
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminUntagAll)
.placeholder("{COUNT}", String.valueOf(combatPlayersSize))
.viewer(sender)
.send();
}
@Execute(name = "stats")
@Permission("eternalcombat.stats")
void stats(@Context CommandSender sender) {
long activeCombatPlayers = this.fightManager.getFights().stream()
.filter(fightTag -> !fightTag.isExpired())
.count();
this.noticeService.create()
.notice(this.config.messagesSettings.admin.combatStats)
.placeholder("{COUNT}", String.valueOf(activeCombatPlayers))
.viewer(sender)
.send();
}
private void tagoutReasonHandler(
CommandSender sender, CancelTagReason cancelReason,
MessagesSettings messagesSettings) {
if (cancelReason == CancelTagReason.TAGOUT) {
this.announcer.create()
.notice(messages.admin.adminTagOutCanceled)
this.noticeService.create()
.notice(messagesSettings.admin.adminTagOutCanceled)
.viewer(sender)
.send();
}
}
}
@@ -1,9 +1,12 @@
package com.eternalcode.combat.fight;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.notification.NotificationAnnouncer;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.util.Optional;
import org.bukkit.Server;
import org.bukkit.entity.Player;
@@ -15,13 +18,13 @@ public class FightTask implements Runnable {
private final Server server;
private final PluginConfig config;
private final FightManager fightManager;
private final NotificationAnnouncer announcer;
private final NoticeService noticeService;
public FightTask(Server server, PluginConfig config, FightManager fightManager, NotificationAnnouncer announcer) {
public FightTask(Server server, PluginConfig config, FightManager fightManager, NoticeService noticeService) {
this.server = server;
this.config = config;
this.fightManager = fightManager;
this.announcer = announcer;
this.noticeService = noticeService;
}
@Override
@@ -40,12 +43,29 @@ public class FightTask implements Runnable {
return;
}
if (this.config.combat.keepCombatActiveInVoid
&& player.getLocation().getY() < this.config.combat.voidCombatHeight
&& !this.config.settings.ignoredWorlds.contains(player.getWorld().getName())) {
FightTagEvent tagEvent = this.fightManager.tag(playerUniqueId, this.config.settings.combatTimerDuration, CauseOfTag.VOID, fightTag.getTagger());
if (!tagEvent.isCancelled()) {
fightTag = this.fightManager.getTag(playerUniqueId);
}
}
Duration remaining = fightTag.getRemainingDuration();
this.announcer.create()
String opponent = Optional.ofNullable(fightTag.getTagger())
.filter(uuid -> !uuid.equals(playerUniqueId))
.map(this.server::getPlayer)
.map(Player::getName)
.orElse(this.config.messagesSettings.unknownPlayerPlaceholder);
this.noticeService.create()
.player(player.getUniqueId())
.notice(this.config.messages.combatNotification)
.placeholder("{TIME}", DurationUtil.format(remaining))
.notice(this.config.messagesSettings.combatNotification)
.placeholder("{TIME}", DurationUtil.format(remaining, this.config.messagesSettings.withoutMillis))
.placeholder("{OPPONENT}", opponent)
.send();
}
@@ -0,0 +1,54 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.WhitelistBlacklistMode;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.util.StringUtil;
public class CommandsBlocker implements Listener {
private final FightManager fightManager;
private final NoticeService noticeService;
private final PluginConfig config;
public CommandsBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config) {
this.fightManager = fightManager;
this.noticeService = noticeService;
this.config = config;
}
@EventHandler
void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
Player player = event.getPlayer();
UUID playerUniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerUniqueId)) {
return;
}
String command = event.getMessage().substring(1);
boolean isAnyMatch = this.config.commands.restrictedCommands.stream()
.anyMatch(restrictedCommand -> StringUtil.startsWithIgnoreCase(command, restrictedCommand));
WhitelistBlacklistMode mode = this.config.commands.commandRestrictionMode;
boolean shouldCancel = mode.shouldBlock(isAnyMatch);
if (shouldCancel) {
event.setCancelled(true);
this.noticeService.create()
.player(playerUniqueId)
.notice(this.config.messagesSettings.commandDisabledDuringCombat)
.send();
}
}
}
@@ -0,0 +1,105 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityToggleGlideEvent;
import org.bukkit.event.player.PlayerMoveEvent;
public class ElytraBlocker implements Listener {
private final FightManager fightManager;
private final PluginConfig config;
private final Cache<UUID, Boolean> fallProtection = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(10))
.build();
public ElytraBlocker(FightManager fightManager, PluginConfig config) {
this.fightManager = fightManager;
this.config = config;
}
@EventHandler
void onToggleGlide(EntityToggleGlideEvent event) {
if (!this.config.combat.disableElytraUsage) {
return;
}
if (!(event.getEntity() instanceof Player player)) {
return;
}
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
if (event.isGliding()) {
event.setCancelled(true);
player.setGliding(false);
}
}
@EventHandler
void onMoveWhileGliding(PlayerMoveEvent event) {
if (!this.config.combat.disableElytraUsage) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
if (player.isGliding()) {
player.setGliding(false);
player.setVelocity(player.getVelocity().setY(-10));
player.setFallDistance(0f);
fallProtection.put(uniqueId, true);
}
}
@EventHandler
public void onEntityDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
if (event.getCause() != EntityDamageEvent.DamageCause.FALL) {
return;
}
if (fallProtection.get(player.getUniqueId(), key -> false)) {
event.setCancelled(true);
}
}
@EventHandler
void onDamage(EntityDamageEvent event) {
if (!this.config.combat.disableElytraOnDamage) {
return;
}
if (!(event.getEntity() instanceof Player player)) {
return;
}
UUID uniqueId = player.getUniqueId();
if (this.fightManager.isInCombat(uniqueId) && player.isGliding()) {
player.setGliding(false);
}
}
}
@@ -0,0 +1,132 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.notification.NoticeService;
import java.util.HashMap;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
public class ElytraEquipBlocker implements Listener {
private final FightManager fightManager;
private final NoticeService noticeService;
private final PluginConfig config;
private final Server server;
public ElytraEquipBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config, Server server) {
this.fightManager = fightManager;
this.noticeService = noticeService;
this.config = config;
this.server = server;
}
@EventHandler
void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
if (!this.config.combat.unequipElytraOnCombat) {
return;
}
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
if (event.getCurrentItem() == null) {
return;
}
if (event.getCurrentItem().getType() == Material.ELYTRA) {
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.elytraDisabledDuringCombat)
.send();
}
}
@EventHandler
void onTag(FightTagEvent event) {
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
if (this.config.combat.unequipElytraOnCombat) {
ItemStack chest = player.getInventory().getChestplate();
if (chest != null && chest.getType() == Material.ELYTRA) {
removeChestplateIfElytra(player);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.elytraDisabledDuringCombat)
.send();
}
}
}
private void removeChestplateIfElytra(Player player) {
ItemStack chestplate = player.getInventory().getChestplate();
if (chestplate != null && chestplate.getType() == Material.ELYTRA) {
player.getInventory().setChestplate(null);
HashMap<Integer, ItemStack> leftover = player.getInventory().addItem(chestplate);
leftover.values().forEach(item ->
player.getWorld().dropItemNaturally(player.getLocation(), item)
);
}
}
@EventHandler
void onInteract(PlayerInteractEvent event) {
if (!this.config.combat.unequipElytraOnCombat) {
return;
}
if (event.getItem() == null) {
return;
}
if (event.getItem().getType() != Material.ELYTRA) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
switch (event.getAction()) {
case RIGHT_CLICK_AIR, RIGHT_CLICK_BLOCK -> {
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.elytraDisabledDuringCombat)
.send();
}
}
}
}
@@ -0,0 +1,86 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import java.util.UUID;
import org.bukkit.GameMode;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerToggleFlightEvent;
public class FlyingBlocker implements Listener {
private final FightManager fightManager;
private final PluginConfig config;
private final Server server;
public FlyingBlocker(FightManager fightManager, PluginConfig config, Server server) {
this.fightManager = fightManager;
this.config = config;
this.server = server;
}
@EventHandler
void onFly(PlayerToggleFlightEvent event) {
if (!this.config.combat.disableFlying) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
if (event.isFlying()) {
player.setAllowFlight(false);
event.setCancelled(true);
}
}
@EventHandler
void onTag(FightTagEvent event) {
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
if (this.config.combat.disableFlying) {
GameMode gameMode = player.getGameMode();
if (gameMode != GameMode.CREATIVE && gameMode != GameMode.SPECTATOR) {
player.setAllowFlight(false);
player.setFlying(false);
}
}
}
@EventHandler
void onUnTag(FightUntagEvent event) {
if (!this.config.combat.disableFlying) {
return;
}
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
GameMode playerGameMode = player.getGameMode();
if (playerGameMode == GameMode.CREATIVE || playerGameMode == GameMode.SPECTATOR) {
player.setAllowFlight(true);
}
}
}
@@ -0,0 +1,49 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.inventory.InventoryType;
public class InventoryContainersBlocker implements Listener {
private final FightManager fightManager;
private final PluginConfig config;
private final NoticeService noticeService;
public InventoryContainersBlocker(FightManager fightManager, PluginConfig config, NoticeService noticeService) {
this.fightManager = fightManager;
this.config = config;
this.noticeService = noticeService;
}
@EventHandler
void onOpenInventory(InventoryOpenEvent event) {
Player player = (Player) event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
InventoryType inventoryType = event.getInventory().getType();
boolean isInRestrictedList = this.config.inventory.restrictedInventoryTypes.contains(inventoryType);
boolean shouldBlock = this.config.inventory.inventoryAccessMode.shouldBlock(isInRestrictedList);
if (shouldBlock) {
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.inventoryBlockedDuringCombat)
.send();
}
}
}
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more