Compare commits

..
98 changed files with 1209 additions and 2965 deletions

No files matched your search

+24 -52
View File
@@ -1,64 +1,36 @@
# 👋 Contributing to EternalCombat
# Contributing to EternalCombat
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.
We welcome all contributions to the EternalCombat project!
## 🛠️ Development Setup
### How to Contribute
To start contributing, you'll need to set up your environment:
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.
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.
### Code of Conduct
### 📥 Getting the Code
We expect all contributors to follow our [code of conduct](CODE_OF_CONDUCT.md).
```bash
# Fork the repository on GitHub first!
# https://github.com/EternalCodeTeam/EternalCombat/fork
git clone https://github.com/YOUR-USERNAME/EternalCombat.git
cd EternalCombat
```
### Reporting Issues
### 🔨 Building the Project
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 messagesSettings or logs.
Run this command in your terminal:
### Compatibility
```bash
./gradlew build
```
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.
## 🤝 How to Contribute
### Submitting a Pull Request
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.
When submitting a pull request, please make sure to follow these guidelines:
## 📏 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).
- 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!
+33
View File
@@ -0,0 +1,33 @@
---
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
@@ -1,84 +0,0 @@
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
@@ -1,8 +0,0 @@
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
@@ -0,0 +1,20 @@
---
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.
@@ -1,38 +0,0 @@
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.
+1 -35
View File
@@ -1,35 +1 @@
---
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)
**Please describe the changes made by this PR and why they need to be merged:**
+21 -27
View File
@@ -5,44 +5,38 @@ on:
branches: [ "master" ]
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Set Variables & Build
uses: actions/github-script@v9
- name: Cache Gradle
uses: actions/cache@v4
with:
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);
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
- name: Build with Gradle
run: ./gradlew shadowJar
- name: Upload build artifact
uses: actions/upload-artifact@v7
uses: gradle/gradle-build-action@093dfe9d598ec5a42246855d09b49dc76803c005
with:
name: ${{ env.FULL_ARTIFACT_NAME }}
arguments: shadowJar
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
with:
name: 'Successfully build EternalCombat'
path: eternalcombat-plugin/build/libs/*.jar
archive: false
-34
View File
@@ -1,34 +0,0 @@
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
@@ -1,34 +0,0 @@
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 }}
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
environment: deployment
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v5
- name: Set up JDK
uses: actions/setup-java@v5
-1
View File
@@ -1,7 +1,6 @@
.classpath
.idea
.gradle
.kotlin
.project
.settings
bin
+16 -32
View File
@@ -9,15 +9,6 @@
[![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>
</div>
### Information
@@ -25,11 +16,11 @@
## EternalCombat requires PacketEvents plugin to work.
You can download it from [here](https://modrinth.com/plugin/packetevents)
EternalCombat 2.0 has been tested on Minecraft versions **1.17.1 to 26.1.2**, but it should work seamlessly on most
EternalCombat 2.0 has been tested on Minecraft versions **1.17.1 to 1.21.9**, 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
The plugin requires **Java 17 or later**, so
ensure your server is ready.
Ready for action?
Install EternalCombat and dive in now!
@@ -43,22 +34,17 @@ fair, heart-pounding battles that keep players on their toes. Here’s a rundown
- **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">
![Combat log anti logout feature](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/combatlog.gif?raw=true)
- **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">
![Border around protected region](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/border.gif?raw=true)
- **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">
everyone in the game until the last anchor hit. Check it out:
![Crystal PvP showcase](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/crystals.gif?raw=true)
- **Fully Customizable Combat**
Tailor the combat experience to your liking with a ton of options! From disabling elytra to setting drop rates for
@@ -82,16 +68,14 @@ EternalCombat in full swing and learn why it’s a game-changer for your server!
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 |
| 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.reload` | Reload the plugin with `/combatlog reload`. |
| `eternalcombat.receiveupdates` | Receive notifications about new plugin versions on join. |
| `eternalcombat.bypass` | Bypass combat tagging, need's `excludeAdminsFromCombat` to be enabled |
## PlaceholderAPI
@@ -139,7 +123,7 @@ With Maven:
With Gradle:
```kts
compileOnly("com.eternalcode:eternalcombat-api:2.5.2-SNAPSHOT")
compileOnly("com.eternalcode:eternalcombat-api:2.3.0")
```
With Maven:
@@ -149,7 +133,7 @@ With Maven:
<dependency>
<groupId>com.eternalcode</groupId>
<artifactId>eternalcombat-api</artifactId>
<version>2.5.2-SNAPSHOT</version>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
```
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

After

Width:  |  Height:  |  Size: 787 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 113 KiB

+3 -5
View File
@@ -8,12 +8,10 @@ repositories {
}
dependencies {
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("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.20")
implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.2.2")
implementation("net.minecrell:plugin-yml:0.6.0")
implementation("xyz.jpenilla:run-task:3.0.2")
implementation("xyz.jpenilla:run-task:3.0.1")
}
sourceSets {
@@ -1,39 +0,0 @@
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"
}
+17 -21
View File
@@ -1,35 +1,31 @@
object Versions {
// Keep this version for best compatibility.
const val PAPER_API = "1.19.3-R0.1-SNAPSHOT"
const val SPIGOT_API = "1.17.1-R0.1-SNAPSHOT"
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 JUNIT_JUPITER_API = "6.0.0"
const val JUNIT_JUPITER_PARAMS = "6.0.0"
const val JUNIT_JUPITER_ENGINE = "6.0.0"
const val JETBRAINS_ANNOTATIONS = "26.1.0"
const val JETBRAINS_ANNOTATIONS = "26.0.2-1"
const val ETERNALCODE_COMMONS = "1.4.1"
const val MULTIFICATION = "1.2.5-SNAPSHOT"
const val PACKETS_EVENTS = "2.13.0"
const val ETERNALCODE_COMMONS = "1.3.1"
const val MULTIFICATION = "1.2.2"
const val PACKETS_EVENTS = "2.11.1"
const val LITE_COMMANDS = "3.11.0"
const val ADVENTURE_PLATFORM_BUKKIT = "4.4.1"
const val ADVENTURE_API = "4.25.0"
const val LITE_COMMANDS = "3.10.5"
const val OKAERI_CONFIGS_SERDES_COMMONS = "5.0.13"
const val OKAERI_CONFIGS_SERDES_BUKKIT = "5.0.13"
const val XSERIES = "13.8.0"
const val CAFFEINE = "3.2.2"
const val CAFFEINE = "3.2.4"
const val B_STATS_BUKKIT = "3.1.0"
const val WORLD_GUARD_BUKKIT = "7.0.14"
const val B_STATS_BUKKIT = "3.2.1"
const val WORLD_GUARD_BUKKIT = "7.0.17"
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"
const val PLACEHOLDER_API = "2.11.6"
const val PAPERLIB = "1.0.8"
}
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
testImplementation("io.papermc.paper:paper-api:${Versions.PAPER_API}")
testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_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}")
@@ -3,7 +3,7 @@ plugins {
}
group = "com.eternalcode"
version = "2.5.2-SNAPSHOT"
version = "2.3.0"
tasks.compileJava {
options.compilerArgs = listOf("-Xlint:deprecation", "-parameters")
@@ -1,28 +0,0 @@
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)
}
}
}
}
}
@@ -1,23 +0,0 @@
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.5.2-SNAPSHOT"
version = "2.3.0"
java {
withSourcesJar()
@@ -3,10 +3,10 @@ plugins {
}
repositories {
mavenLocal()
mavenCentral()
maven("https://repo.eternalcode.pl/releases")
maven("https://repo.eternalcode.pl/snapshots")
mavenCentral()
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/")
@@ -1,22 +0,0 @@
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 {
// Paper api
compileOnlyApi("io.papermc.paper:paper-api:${Versions.PAPER_API}")
// Spigot api
compileOnlyApi("org.spigotmc:spigot-api:${Versions.SPIGOT_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.PearlService;
import com.eternalcode.combat.fight.pearl.FightPearlService;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
@@ -14,7 +14,7 @@ public interface EternalCombatApi {
RegionProvider getRegionProvider();
PearlService getFightPearlService();
FightPearlService getFightPearlService();
FightTagOutService getFightTagOutService();
@@ -2,9 +2,6 @@ package com.eternalcode.combat.fight.event;
public enum CancelTagReason {
TAGOUT,
PERMISSION_BYPASS,
ADMIN,
CREATIVE_MODE,
TAGOUT
}
@@ -23,22 +23,6 @@ public enum CauseOfTag {
*/
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 PearlService {
public interface FightPearlService {
boolean shouldCancelEvent(UUID playerId);
Instant getDelay(UUID uuid);
Duration getRemainingDelay(UUID uuid);
boolean hasDelay(UUID uuid);
void handleDelay(Player uuid);
void markDelay(UUID uuid);
}
@@ -0,0 +1,16 @@
package com.eternalcode.combat.fight.trident;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
public interface FightTridentService {
Instant getDelay(UUID uuid);
Duration getRemainingDelay(UUID uuid);
boolean hasDelay(UUID uuid);
void markDelay(UUID uuid);
}
@@ -1,31 +0,0 @@
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);
}
+30 -46
View File
@@ -1,16 +1,12 @@
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("de.eldoria.plugin-yml.paper") version "0.9.0"
id("net.minecrell.plugin-yml.bukkit")
id("com.gradleup.shadow")
id("xyz.jpenilla.run-paper")
}
configurations.all {
@@ -27,8 +23,14 @@ configurations.all {
dependencies {
implementation(project(":eternalcombat-api"))
// Paper
compileOnly("io.papermc.paper:paper-api:${Versions.PAPER_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)
}
}
// litecommands
implementation("dev.rollczi:litecommands-bukkit:${Versions.LITE_COMMANDS}")
@@ -38,9 +40,6 @@ dependencies {
implementation("eu.okaeri:okaeri-configs-serdes-commons:${Versions.OKAERI_CONFIGS_SERDES_COMMONS}")
implementation("eu.okaeri:okaeri-configs-serdes-bukkit:${Versions.OKAERI_CONFIGS_SERDES_BUKKIT}")
// XSeries
implementation("com.github.cryptomorin:XSeries:${Versions.XSERIES}")
// bstats
implementation("org.bstats:bstats-bukkit:${Versions.B_STATS_BUKKIT}")
@@ -58,54 +57,41 @@ dependencies {
// PlaceholderAPI
compileOnly("me.clip:placeholderapi:${Versions.PLACEHOLDER_API}")
// Lands
compileOnly("com.github.angeschossen:LandsAPI:${Versions.LANDS_API}")
compileOnly("com.github.angeschossen:LandsAPI:7.17.2")
// Multification
implementation("com.eternalcode:multification-paper:${Versions.MULTIFICATION}")
implementation("com.eternalcode:multification-bukkit:${Versions.MULTIFICATION}")
implementation("com.eternalcode:multification-okaeri:${Versions.MULTIFICATION}")
compileOnly("com.github.retrooper:packetevents-spigot:${Versions.PACKETS_EVENTS}")
implementation("io.papermc:paperlib:${Versions.PAPERLIB}")
}
paper {
bukkit {
main = "com.eternalcode.combat.CombatPlugin"
authors = listOf("EternalCodeTeam")
apiVersion = "1.19"
author = "EternalCodeTeam"
apiVersion = "1.13"
prefix = "EternalCombat"
name = "EternalCombat"
generateLibrariesJson = true
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
softDepend = listOf(
"Lands"
)
depend = listOf(
"packetevents",
)
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 {
named("generatePaperPluginDescription") {
notCompatibleWithConfigurationCache("The plugin-yml paper generator reads Task.project during execution.")
runServer {
minecraftVersion("1.21.10")
downloadPlugins.url("https://cdn.modrinth.com/data/1u6JkXh5/versions/Jk1z2u7n/worldedit-bukkit-7.3.16.jar")
downloadPlugins.modrinth("packetevents", "${Versions.PACKETS_EVENTS}+spigot")
downloadPlugins.url("https://cdn.modrinth.com/data/DKY9btbd/versions/PO4MKx7e/worldguard-bukkit-7.0.14-dist.jar")
}
}
@@ -115,18 +101,17 @@ tasks.shadowJar {
exclude(
"org/intellij/lang/annotations/**",
"org/jetbrains/annotations/**",
"net/kyori/**",
"META-INF/**",
"kotlin/**",
"javax/**",
"org/checkerframework/**",
"com/google/errorprone/**",
"com/google/gson/**"
)
val prefix = "com.eternalcode.combat.libs"
listOf(
"eu.okaeri",
"net.kyori",
"org.bstats",
"org.yaml",
"dev.rollczi.litecommands",
@@ -135,8 +120,7 @@ tasks.shadowJar {
"com.github.benmanes.caffeine",
"com.eternalcode.commons",
"com.eternalcode.multification",
"com.github.cryptomorin",
"com.cryptomorin",
"io.papermc.lib"
).forEach { pack ->
relocate(pack, "$prefix.$pack")
}
@@ -1,64 +1,52 @@
package com.eternalcode.combat;
import com.eternalcode.combat.border.BorderTriggerController;
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.crystalpvp.RespawnAnchorListener;
import com.eternalcode.combat.crystalpvp.EndCrystalListener;
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.firework.FireworkController;
import com.eternalcode.combat.fight.knockback.KnockbackService;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
import com.eternalcode.combat.fight.pearl.FightPearlService;
import com.eternalcode.combat.fight.trident.FightTridentController;
import com.eternalcode.combat.fight.trident.FightTridentService;
import com.eternalcode.combat.fight.trident.FightTridentServiceImpl;
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.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.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.FightTagCommand;
import com.eternalcode.combat.fight.controller.FightActionBlockerController;
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.effect.FightEffectController;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.fight.FightManagerImpl;
import com.eternalcode.combat.fight.FightTask;
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.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.fight.pearl.FightPearlController;
import com.eternalcode.combat.fight.pearl.FightPearlServiceImpl;
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.fight.tagout.FightTagOutCommand;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.fight.knockback.KnockbackRegionController;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.updater.UpdaterNotificationController;
import com.eternalcode.combat.updater.UpdaterService;
@@ -72,6 +60,8 @@ import dev.rollczi.litecommands.bukkit.LiteBukkitFactory;
import dev.rollczi.litecommands.bukkit.LiteBukkitMessages;
import dev.rollczi.litecommands.folia.FoliaExtension;
import java.time.Duration;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bstats.bukkit.Metrics;
import org.bukkit.Server;
@@ -90,8 +80,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
private static final int BSTATS_METRICS_ID = 17803;
private FightManager fightManager;
private PearlService pearlService;
private TridentService tridentService;
private FightPearlService fightPearlService;
private FightTridentService fightTridentService;
private FightTagOutService fightTagOutService;
private FightEffectService fightEffectService;
@@ -100,8 +90,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
private RegionProvider regionProvider;
private AudienceProvider audienceProvider;
private LiteCommands<CommandSender> liteCommands;
private boolean apiInitialized;
@Override
@@ -119,8 +109,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
MinecraftScheduler scheduler = CombatSchedulerAdapter.getAdaptiveScheduler(this);
this.fightManager = new FightManagerImpl(eventManager);
this.pearlService = new PearlServiceImpl(this.fightManager, pluginConfig, scheduler);
this.tridentService = new TridentServiceImpl(pluginConfig);
this.fightPearlService = new FightPearlServiceImpl(pluginConfig.pearl);
this.fightTridentService = new FightTridentServiceImpl(pluginConfig.trident);
this.fightTagOutService = new FightTagOutServiceImpl();
this.fightEffectService = new FightEffectServiceImpl();
@@ -131,12 +121,13 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
UpdaterService updaterService = new UpdaterService(this.getDescription());
this.audienceProvider = BukkitAudiences.create(this);
MiniMessage miniMessage = MiniMessage.builder()
.postProcessor(new AdventureLegacyColorPostProcessor())
.preProcessor(new AdventureLegacyColorPreProcessor())
.build();
NoticeService noticeService = new NoticeService(pluginConfig, miniMessage);
NoticeService noticeService = new NoticeService(this.audienceProvider, pluginConfig, miniMessage);
BridgeService bridgeService = new BridgeService(
pluginConfig,
@@ -186,36 +177,22 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
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 FightActionBlockerController(this.fightManager, noticeService, pluginConfig, server),
new FightPearlController(pluginConfig.pearl, noticeService, this.fightManager, this.fightPearlService, pluginConfig),
new FightTridentController(pluginConfig.trident, noticeService, this.fightManager, this.fightTridentService, pluginConfig),
new UpdaterNotificationController(updaterService, pluginConfig, this.audienceProvider, miniMessage),
new KnockbackRegionController(noticeService, this.regionProvider, this.fightManager, knockbackService, server),
new FightEffectController(pluginConfig.effect, this.fightEffectService, this.fightManager, server),
new FightEffectController(pluginConfig.effect, this.fightEffectService, this.fightManager, this.getServer()),
new FightTagOutController(this.fightTagOutService),
new FightMessageController(this.fightManager, noticeService, pluginConfig, server),
new FightMessageController(this.fightManager, noticeService, pluginConfig, this.getServer()),
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 FireworkController(this.fightManager, pluginConfig, noticeService)
);
new KnockbackMountController(noticeService, this.regionProvider, this.fightManager).register(this);
eventManager.subscribe(
PlayerDeathEvent.class,
pluginConfig.drop.dropEventPriority,
@@ -229,7 +206,6 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
);
EternalCombatProvider.initialize(this);
this.apiInitialized = true;
long millis = started.elapsed(TimeUnit.MILLISECONDS);
this.getLogger().info("Successfully loaded EternalCombat in " + millis + "ms");
@@ -237,18 +213,17 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
@Override
public void onDisable() {
EternalCombatProvider.deinitialize();
if (this.liteCommands != null) {
this.liteCommands.unregister();
}
if (this.fightManager != null) {
this.fightManager.untagAll();
if (this.audienceProvider != null) {
this.audienceProvider.close();
}
if (this.apiInitialized) {
EternalCombatProvider.deinitialize();
this.apiInitialized = false;
}
this.fightManager.untagAll();
}
@Override
@@ -262,8 +237,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
}
@Override
public PearlService getFightPearlService() {
return this.pearlService;
public FightPearlService getFightPearlService() {
return this.fightPearlService;
}
@Override
@@ -2,6 +2,7 @@ package com.eternalcode.combat;
import com.eternalcode.combat.config.ConfigService;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.multification.bukkit.notice.BukkitNotice;
import com.eternalcode.multification.notice.Notice;
import com.google.common.base.Stopwatch;
import dev.rollczi.litecommands.annotations.async.Async;
@@ -15,7 +16,7 @@ import org.bukkit.command.CommandSender;
@Command(name = "combatlog", aliases = "combat")
public class EternalCombatReloadCommand {
private static final Notice RELOAD_MESSAGE = Notice.builder()
private static final Notice RELOAD_MESSAGE = BukkitNotice.builder()
.chat("<b><gradient:#8a1212:#fc6b03>EternalCombat:</gradient></b> Reloaded EternalCombat in <color:#fce303>{TIME}ms!</color>")
.build();
@@ -2,24 +2,18 @@ package com.eternalcode.combat.border;
import org.jetbrains.annotations.Nullable;
public record BorderPoint(int x, int y, int z, @Nullable BorderPoint innerPoint) {
public record BorderPoint(int x, int y, int z, @Nullable BorderPoint inclusive) {
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) {
public BorderPoint toInclusive() {
if (inclusive == null) {
return this;
}
return this.innerPoint;
return inclusive;
}
}
@@ -129,8 +129,8 @@ public class BorderServiceImpl implements BorderService {
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);
BorderPoint inclusive = new BorderPoint(Math.max(realMinX, currentX - 1), realMaxY - 1, Math.max(realMinZ, currentZ - 1));
addPoint(points, currentX, realMaxY, currentZ, playerLocation, inclusive);
}
}
}
@@ -146,8 +146,8 @@ public class BorderServiceImpl implements BorderService {
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);
BorderPoint inclusive = new BorderPoint(realMaxX - 1, Math.max(realMinY, currentY - 1), Math.max(realMinZ, currentZ - 1));
addPoint(points, realMaxX, currentY, currentZ, playerLocation, inclusive);
}
}
}
@@ -163,8 +163,8 @@ public class BorderServiceImpl implements BorderService {
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);
BorderPoint inclusive = new BorderPoint(Math.max(realMinX, currentX - 1), Math.max(realMinY, currentY - 1), realMaxZ - 1);
addPoint(points, currentX, currentY, realMaxZ, playerLocation, inclusive);
}
}
}
@@ -172,9 +172,9 @@ public class BorderServiceImpl implements BorderService {
return points;
}
private void addPoint(List<BorderPoint> points, int x, int y, int z, Location playerLocation, BorderPoint innerPoint) {
private void addPoint(List<BorderPoint> points, int x, int y, int z, Location playerLocation, BorderPoint inclusive) {
if (isVisible(x, y, z, playerLocation)) {
points.add(new BorderPoint(x, y, z, innerPoint));
points.add(new BorderPoint(x, y, z, inclusive));
}
}
@@ -15,7 +15,6 @@ 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;
@@ -26,12 +25,11 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Material;
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 {
@@ -39,7 +37,7 @@ public class BorderBlockController implements Listener {
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 AIR_ID = WrappedBlockState.getDefaultState(SERVER_VERSION, StateTypes.AIR).getGlobalId();
public static final int MINECRAFT_CHUNK_SHIFT = 4;
private final BorderService borderService;
@@ -48,7 +46,6 @@ public class BorderBlockController implements Listener {
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) {
@@ -62,52 +59,37 @@ public class BorderBlockController implements Listener {
@EventHandler
void onBorderShowAsyncEvent(BorderShowAsyncEvent event) {
if (!this.settings.get().enabled) {
if (!settings.get().enabled) {
return;
}
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
Set<BorderPoint> borderPoints = this.getPointsWithoutAir(player, event.getPoints());
synchronized (this.getLock(playerId)) {
Set<BorderPoint> borderPoints = this.savePassablePoints(player, event.getPoints());
event.setPoints(borderPoints);
this.showBorder(player, borderPoints);
this.playersToUpdate.add(playerId);
}
event.setPoints(borderPoints);
this.showBlocks(player, borderPoints);
this.playersToUpdate.add(player.getUniqueId());
}
@EventHandler
void onBorderHideAsyncEvent(BorderHideAsyncEvent event) {
if (!this.settings.get().enabled) {
if (!settings.get().enabled) {
return;
}
UUID playerId = event.getPlayer().getUniqueId();
synchronized (this.getLock(playerId)) {
this.hideBorder(event.getPlayer(), event.getPoints());
Object lock = lockedPlayers.computeIfAbsent(event.getPlayer().getUniqueId(), k -> new Object());
synchronized (lock) {
this.hideBlocks(event.getPlayer(), event.getPoints());
Set<BorderPoint> border = this.borderService.getActiveBorder(event.getPlayer());
if (border.isEmpty()) {
this.playersToUpdate.remove(playerId);
this.originalBlocks.remove(playerId);
this.playersToUpdate.remove(event.getPlayer().getUniqueId());
}
}
}
@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) {
if (!settings.get().enabled) {
return;
}
@@ -123,109 +105,93 @@ public class BorderBlockController implements Listener {
}
private void updatePlayer(UUID uuid, Player player) {
synchronized (this.getLock(uuid)) {
Object lock = lockedPlayers.computeIfAbsent(uuid, k -> new Object());
synchronized (lock) {
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);
this.showBlocks(player, border);
}
}
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 showBlocks(Player player, Collection<BorderPoint> blocks) {
this.splitByChunks(blocks).entrySet().stream()
.map(chunkBlocks -> toMultiBlockChangePacket(chunkBlocks))
.forEach(change -> PLAYER_MANAGER.sendPacket(player, change));
}
private void hideBorder(Player player, Collection<BorderPoint> blocks) {
UUID playerId = player.getUniqueId();
Map<BorderPoint, WrappedBlockState> savedBlocks = this.originalBlocks.get(playerId);
private WrapperPlayServerMultiBlockChange toMultiBlockChangePacket(Entry<Vector3i, Set<BorderPoint>> chunkBlocks) {
EncodedBlock[] encodedBlocks = chunkBlocks.getValue().stream()
.map(borderPoint -> this.toEncodedBlock(borderPoint))
.toArray(value -> new EncodedBlock[value]);
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));
}
return new WrapperPlayServerMultiBlockChange(chunkBlocks.getKey(), true, encodedBlocks);
}
private Set<BorderPoint> savePassablePoints(Player player, Collection<BorderPoint> points) {
UUID playerId = player.getUniqueId();
Map<BorderPoint, WrappedBlockState> savedBlocks = this.originalBlocks.computeIfAbsent(playerId, key -> new ConcurrentHashMap<>());
private void hideBlocks(Player player, Collection<BorderPoint> blocks) {
this.splitByChunks(blocks).entrySet().stream()
.map(chunkBlocks -> toMultiAirChangePacket(chunkBlocks))
.forEach(change -> PLAYER_MANAGER.sendPacket(player, change));
}
return this.groupByChunk(points).entrySet().stream()
.flatMap(entry -> this.savePassableBlock(player, entry, savedBlocks))
private WrapperPlayServerMultiBlockChange toMultiAirChangePacket(Entry<Vector3i, Set<BorderPoint>> chunkBlocks) {
EncodedBlock[] encodedBlocks = chunkBlocks.getValue().stream()
.map(point -> new EncodedBlock(AIR_ID, point.x(), point.y(), point.z()))
.toArray(value -> new EncodedBlock[value]);
return new WrapperPlayServerMultiBlockChange(chunkBlocks.getKey(), true, encodedBlocks);
}
private Set<BorderPoint> getPointsWithoutAir(Player player, Collection<BorderPoint> blocks) {
Map<ChunkLocation, Set<BorderPoint>> chunksToProcess = blocks.stream()
.map(borderPoint -> borderPoint.toInclusive())
.collect(Collectors.groupingBy(
inclusive -> new ChunkLocation(inclusive.x() >> MINECRAFT_CHUNK_SHIFT, inclusive.z() >> MINECRAFT_CHUNK_SHIFT),
Collectors.toSet()
));
return chunksToProcess.entrySet().stream()
.flatMap(entry -> getPointsWithoutAirOnChunk(player, entry))
.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());
private Stream<BorderPoint> getPointsWithoutAirOnChunk(Player player, Entry<ChunkLocation, Set<BorderPoint>> entry) {
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));
return entry.getValue().stream()
.filter(point -> isAir(entry.getKey(), point, snapshot));
}
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);
private static boolean isAir(ChunkLocation location, BorderPoint point, ChunkSnapshot chunk) {
int xInsideChunk = point.x() - (location.x() << MINECRAFT_CHUNK_SHIFT);
int zInsideChunk = point.z() - (location.z() << MINECRAFT_CHUNK_SHIFT);
Material material = chunk.getBlockType(xInsideChunk, point.y(), zInsideChunk);
BlockData blockData = snapshot.getBlockData(localX, point.y(), localZ);
if (blockData.getMaterial().isSolid()) {
return false;
}
savedBlocks.put(point, SpigotConversionUtil.fromBukkitBlockData(blockData));
return true;
return material.isAir();
}
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),
private Map<Vector3i, Set<BorderPoint>> splitByChunks(Collection<BorderPoint> blocks) {
return blocks.stream().collect(Collectors.groupingBy(
block -> new Vector3i(
block.x() >> MINECRAFT_CHUNK_SHIFT,
block.y() >> MINECRAFT_CHUNK_SHIFT,
block.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 EncodedBlock toEncodedBlock(BorderPoint point) {
StateType type = settings.get().type.getStateType(point);
WrappedBlockState state = WrappedBlockState.getDefaultState(SERVER_VERSION, type);
return new EncodedBlock(state.getGlobalId(), point.x(), point.y(), point.z());
}
private Object getLock(UUID uuid) {
return this.lockedPlayers.computeIfAbsent(uuid, k -> new Object());
}
}
@@ -2,6 +2,7 @@ package com.eternalcode.combat.border.animation.block;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.papermc.lib.PaperLib;
import org.bukkit.ChunkSnapshot;
import org.bukkit.entity.Player;
@@ -21,7 +22,7 @@ class ChunkCache {
return snapshot;
}
ChunkSnapshot chunkSnapshot = player.getWorld().getChunkAtAsync(location.x(), location.z(), false)
ChunkSnapshot chunkSnapshot = PaperLib.getChunkAtAsync(player.getWorld(), location.x(), location.z(), false)
.thenApply(chunk -> chunk.getChunkSnapshot())
.join();
@@ -3,9 +3,9 @@ package com.eternalcode.combat.config;
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.bukkit.notice.resolver.sound.SoundBukkitResolver;
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;
@@ -28,7 +28,7 @@ public class ConfigService {
YamlSnakeYamlConfigurer configurer = new YamlSnakeYamlConfigurer();
NoticeResolverRegistry noticeRegistry = NoticeResolverDefaults.createRegistry()
.registerResolver(new SoundAdventureResolver());
.registerResolver(new SoundBukkitResolver());
configFile.withConfigurer(configurer,
new SerdesCommons(),
@@ -33,20 +33,18 @@ public class CombatSettings extends OkaeriConfig {
})
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({
"# Prevent players from opening their inventory during combat.",
"# Set to 'true' to block inventory access while in combat."
})
public boolean disableInventoryAccess = false;
@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."
@@ -90,19 +88,6 @@ public class CombatSettings extends OkaeriConfig {
})
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.",
@@ -1,38 +0,0 @@
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;
};
}
}
@@ -1,38 +0,0 @@
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
);
}
@@ -1,5 +1,6 @@
package com.eternalcode.combat.config.implementation;
import com.eternalcode.multification.bukkit.notice.BukkitNotice;
import com.eternalcode.multification.notice.Notice;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
@@ -7,205 +8,189 @@ 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."
"# 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",
" ",
" ",
"# Configure the combat log notification displayed to players.",
"# You can use the {TIME} variable to display the remaining combat time.",
" ",
})
public Notice combatNotification = Notice.builder()
.actionBar("Combat ends in: <red>{TIME}</red>")
.build();
public Notice combatNotification = BukkitNotice.builder()
.actionBar("Combat ends in: <red>{TIME}</red>")
.build();
@Comment({
"# Would you like to display milliseconds instead of seconds in combat notification "
"# 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."
"# 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>");
"<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."
"# 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>");
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."
"# 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>");
"<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."
"# 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>");
"<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."
"# 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>");
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."
"# 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>");
"<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."
"# 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."
"# 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>");
"<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."
"# 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>");
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"
"# 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>");
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."
"# 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>");
"<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."
"# 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>");
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."
"# 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>");
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."
"# 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>");
"<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."
"# 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>");
"<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."
"# 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>");
"<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."
"# 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>");
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."
"# 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>");
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."
"# 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>");
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."
"# 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."
"# 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>");
"<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."
"# 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>");
"<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."
"# 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>");
"<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."
"# 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>");
@@ -220,14 +205,7 @@ public class MessagesSettings extends OkaeriConfig {
"# 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>");
public Notice adminTagOutCanceled =
Notice.chat("<gradient:red:dark_red>❌ Player has tag-out protection!</gradient>");
}
}
@@ -1,12 +1,11 @@
package com.eternalcode.combat.config.implementation;
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.knockback.KnockbackSettings;
import com.eternalcode.combat.fight.pearl.PearlSettings;
import com.eternalcode.combat.fight.trident.TridentSettings;
import com.eternalcode.combat.fight.pearl.FightPearlSettings;
import com.eternalcode.combat.fight.trident.FightTridentSettings;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
@@ -35,14 +34,14 @@ public class PluginConfig extends OkaeriConfig {
"# Settings related to Ender Pearls.",
"# Configure cooldowns, restrictions, and other behaviors for Ender Pearls during combat."
})
public PearlSettings pearl = new PearlSettings();
public FightPearlSettings pearl = new FightPearlSettings();
@Comment({
" ",
"# Settings related to Trident",
"# Configure cooldowns, restrictions, and other behaviors for Trident during combat."
})
public TridentSettings trident = new TridentSettings();
public FightTridentSettings trident = new FightTridentSettings();
@Comment({
" ",
@@ -51,12 +50,6 @@ public class PluginConfig extends OkaeriConfig {
})
public FightEffectSettings effect = new FightEffectSettings();
@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.",
@@ -120,20 +113,6 @@ public class PluginConfig extends OkaeriConfig {
})
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.",
@@ -1,13 +0,0 @@
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;
}
@@ -3,11 +3,12 @@ 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 com.eternalcode.combat.util.ReflectUtil;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.EnderCrystal;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByBlockEvent;
@@ -22,6 +23,21 @@ public class CrystalPvpConstants {
public static final String CRYSTAL_METADATA = "eternalcombat:crystal";
public static final String ANCHOR_METADATA = "eternalcombat:anchor";
private static final boolean HAS_DAMAGER_BLOCK_STATE = checkForDamagerBlockState();
private static boolean checkForDamagerBlockState() {
try {
return EntityDamageByBlockEvent.class.getDeclaredMethod("getDamagerBlockState") != null;
}
catch (NoSuchMethodException exception) {
return false;
}
}
static boolean hasDamagerBlockState() {
return HAS_DAMAGER_BLOCK_STATE;
}
public static Optional<UUID> getDamagerUniqueIdFromEndCrystal(EntityDamageByEntityEvent event) {
if (event.getDamager() instanceof EnderCrystal enderCrystal) {
List<MetadataValue> metadataValues = enderCrystal.getMetadata(CRYSTAL_METADATA);
@@ -36,16 +52,20 @@ public class CrystalPvpConstants {
}
public static Optional<UUID> getDamagerUniqueIdFromRespawnAnchor(EntityDamageByBlockEvent event) {
Block damager = event.getDamager();
if (damager == null) {
if (!CrystalPvpConstants.hasDamagerBlockState()) {
return Optional.empty();
}
if (damager.getType() != Material.RESPAWN_ANCHOR) {
Object maybeState = ReflectUtil.invokeMethod(event, "getDamagerBlockState");
if (!(maybeState instanceof BlockState state)) {
return Optional.empty();
}
Material type = state.getType();
if (!type.equals(Material.RESPAWN_ANCHOR)) {
return Optional.empty();
}
return damager.getMetadata(ANCHOR_METADATA).stream()
return state.getMetadata(ANCHOR_METADATA).stream()
.filter(source -> source instanceof CrystalMetadata)
.map(meta -> (CrystalMetadata) meta)
.findFirst()
@@ -16,8 +16,10 @@ import dev.rollczi.litecommands.annotations.permission.Permission;
import dev.rollczi.litecommands.annotations.priority.Priority;
import dev.rollczi.litecommands.annotations.priority.PriorityValue;
import java.time.Duration;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -47,6 +49,7 @@ public class FightTagCommand {
.placeholder("{PLAYER}", target.getName())
.viewer(sender)
.send();
}
@Execute(name = "tag")
@@ -71,6 +74,7 @@ public class FightTagCommand {
.placeholder("{PLAYER}", target.getName())
.viewer(sender)
.send();
}
@Execute(name = "tag")
@@ -118,6 +122,7 @@ public class FightTagCommand {
.placeholder("{SECOND_PLAYER}", secondTarget.getName())
.viewer(sender)
.send();
}
@Execute(name = "untag")
@@ -139,6 +144,7 @@ public class FightTagCommand {
return;
}
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminUntagPlayer)
.placeholder("{PLAYER}", target.getName())
@@ -163,28 +169,13 @@ public class FightTagCommand {
.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) {
private void tagoutReasonHandler(CommandSender sender, CancelTagReason cancelReason, MessagesSettings messagesSettings) {
if (cancelReason == CancelTagReason.TAGOUT) {
this.noticeService.create()
.notice(messagesSettings.admin.adminTagOutCanceled)
.viewer(sender)
.send();
}
}
}
@@ -1,12 +1,9 @@
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.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;
@@ -20,7 +17,7 @@ public class FightTask implements Runnable {
private final FightManager fightManager;
private final NoticeService noticeService;
public FightTask(Server server, PluginConfig config, FightManager fightManager, NoticeService noticeService) {
public FightTask(Server server, PluginConfig config, FightManager fightManager, NoticeService noticeService) {
this.server = server;
this.config = config;
this.fightManager = fightManager;
@@ -43,29 +40,12 @@ 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();
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.messagesSettings.combatNotification)
.placeholder("{TIME}", DurationUtil.format(remaining, this.config.messagesSettings.withoutMillis))
.placeholder("{OPPONENT}", opponent)
.send();
}
@@ -1,54 +0,0 @@
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();
}
}
}
@@ -1,105 +0,0 @@
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);
}
}
}
@@ -1,132 +0,0 @@
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();
}
}
}
}
@@ -1,86 +0,0 @@
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);
}
}
}
@@ -1,49 +0,0 @@
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();
}
}
}
@@ -1,81 +0,0 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.config.implementation.BlockPlacementSettings;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
import java.util.List;
import java.util.UUID;
public class PlaceBlockBlocker implements Listener {
private final FightManager fightManager;
private final NoticeService noticeService;
private final PluginConfig config;
public PlaceBlockBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config) {
this.fightManager = fightManager;
this.noticeService = noticeService;
this.config = config;
}
@EventHandler
void onPlace(BlockPlaceEvent event) {
if (!this.config.blockPlacement.disableBlockPlacing) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
Block block = event.getBlock();
int level = block.getY();
List<Material> specificBlocksToPreventPlacing = this.config.blockPlacement.restrictedBlockTypes;
boolean isPlacementBlocked = this.isPlacementBlocked(level);
if (isPlacementBlocked && specificBlocksToPreventPlacing.isEmpty()) {
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat)
.placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate))
.placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName)
.send();
}
Material blockMaterial = block.getType();
boolean isBlockInDisabledList = specificBlocksToPreventPlacing.contains(blockMaterial);
if (isPlacementBlocked && isBlockInDisabledList) {
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat)
.placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate))
.placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName)
.send();
}
}
private boolean isPlacementBlocked(int level) {
return this.config.blockPlacement.blockPlacementMode == BlockPlacementSettings.BlockPlacingMode.ABOVE
? level > this.config.blockPlacement.blockPlacementYCoordinate
: level < this.config.blockPlacement.blockPlacementYCoordinate;
}
}
@@ -1,63 +0,0 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Event.Result;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
public class SignEditingBlocker implements Listener {
private final FightManager fightManager;
private final PluginConfig config;
public SignEditingBlocker(FightManager fightManager, PluginConfig config) {
this.fightManager = fightManager;
this.config = config;
}
@EventHandler(ignoreCancelled = true)
void onInteract(PlayerInteractEvent event) {
if (!this.config.signEditing.disableSignEditingDuringCombat) {
return;
}
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
Block clickedBlock = event.getClickedBlock();
if (clickedBlock == null || !isSign(clickedBlock.getType())) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
event.setUseInteractedBlock(Result.DENY);
ItemStack item = event.getItem();
if (item != null && isEnderPearl(item.getType()) && event.useItemInHand() != Result.DENY) {
event.setUseItemInHand(Result.ALLOW);
}
}
static boolean isSign(Material material) {
return material.name().endsWith("_SIGN");
}
static boolean isEnderPearl(Material material) {
return material == Material.ENDER_PEARL;
}
}
@@ -0,0 +1,237 @@
package com.eternalcode.combat.fight.controller;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.WhitelistBlacklistMode;
import com.eternalcode.combat.config.implementation.BlockPlacementSettings;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import com.eternalcode.combat.notification.NoticeService;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityToggleGlideEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerToggleFlightEvent;
import java.util.List;
import java.util.UUID;
import org.bukkit.util.StringUtil;
public class FightActionBlockerController implements Listener {
private final FightManager fightManager;
private final NoticeService noticeService;
private final PluginConfig config;
private final Server server;
public FightActionBlockerController(FightManager fightManager, NoticeService noticeService, PluginConfig config, Server server) {
this.fightManager = fightManager;
this.noticeService = noticeService;
this.config = config;
this.server = server;
}
@EventHandler
void onPlace(BlockPlaceEvent event) {
if (!this.config.blockPlacement.disableBlockPlacing) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
Block block = event.getBlock();
int level = block.getY();
List<Material> specificBlocksToPreventPlacing = this.config.blockPlacement.restrictedBlockTypes;
boolean isPlacementBlocked = this.isPlacementBlocked(level);
if (isPlacementBlocked && specificBlocksToPreventPlacing.isEmpty()) {
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat)
.placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate))
.placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName)
.send();
}
Material blockMaterial = block.getType();
boolean isBlockInDisabledList = specificBlocksToPreventPlacing.contains(blockMaterial);
if (isPlacementBlocked && isBlockInDisabledList) {
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat)
.placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate))
.placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName)
.send();
}
}
private boolean isPlacementBlocked(int level) {
return this.config.blockPlacement.blockPlacementMode == BlockPlacementSettings.BlockPlacingMode.ABOVE
? level > this.config.blockPlacement.blockPlacementYCoordinate
: level < this.config.blockPlacement.blockPlacementYCoordinate;
}
@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);
}
}
@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);
}
}
@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 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);
}
}
@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);
}
}
@EventHandler
void onOpenInventory(InventoryOpenEvent event) {
if (!this.config.combat.disableInventoryAccess) {
return;
}
Player player = (Player) event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
event.setCancelled(true);
this.noticeService.create()
.player(uniqueId)
.notice(this.config.messagesSettings.inventoryBlockedDuringCombat)
.send();
}
@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();
}
}
}
@@ -1,37 +0,0 @@
package com.eternalcode.combat.fight.controller;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CancelTagReason;
import com.eternalcode.combat.fight.event.FightTagEvent;
import java.util.UUID;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
public class FightBypassAdminController implements Listener {
private final Server server;
private final PluginConfig config;
public FightBypassAdminController(Server server, PluginConfig config) {
this.server = server;
this.config = config;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
void onFightTagEvent(FightTagEvent event) {
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
if (this.config.admin.excludeAdminsFromCombat && player.isOp()) {
event.setCancelReason(CancelTagReason.ADMIN);
event.setCancelled(true);
}
}
}
@@ -1,39 +0,0 @@
package com.eternalcode.combat.fight.controller;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CancelTagReason;
import com.eternalcode.combat.fight.event.FightTagEvent;
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.EventPriority;
import org.bukkit.event.Listener;
public class FightBypassCreativeController implements Listener {
private final Server server;
private final PluginConfig config;
public FightBypassCreativeController(Server server, PluginConfig config) {
this.server = server;
this.config = config;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
void onFightTagEvent(FightTagEvent event) {
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
if (this.config.admin.excludeCreativePlayersFromCombat && player.getGameMode() == GameMode.CREATIVE) {
event.setCancelReason(CancelTagReason.CREATIVE_MODE);
event.setCancelled(true);
}
}
}
@@ -1,39 +0,0 @@
package com.eternalcode.combat.fight.controller;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CancelTagReason;
import com.eternalcode.combat.fight.event.FightTagEvent;
import java.util.UUID;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
public class FightBypassPermissionController implements Listener {
private static final String BYPASS_PERMISSION = "eternalcombat.bypass";
private final Server server;
private final PluginConfig config;
public FightBypassPermissionController(Server server, PluginConfig config) {
this.server = server;
this.config = config;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
void onFightTagEvent(FightTagEvent event) {
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
if (player.hasPermission(BYPASS_PERMISSION) && this.config.admin.excludeAdminsFromCombat) {
event.setCancelReason(CancelTagReason.PERMISSION_BYPASS);
event.setCancelled(true);
}
}
}
@@ -4,6 +4,7 @@ import com.eternalcode.combat.WhitelistBlacklistMode;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import org.bukkit.GameMode;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
@@ -55,6 +56,14 @@ public class FightTagController implements Listener {
return;
}
if (this.cannotBeTagged(attacker)) {
return;
}
if (this.cannotBeTagged(attackedPlayerByPerson)) {
return;
}
if (this.config.combat.disableFlying) {
if (attackedPlayerByPerson.isFlying()) {
attackedPlayerByPerson.setFlying(false);
@@ -90,6 +99,15 @@ public class FightTagController implements Listener {
return;
}
if (this.cannotBeTagged(player)) {
return;
}
boolean hasBypass = player.hasPermission("eternalcombat.bypass");
if (hasBypass) {
return;
}
Duration combatTime = this.config.settings.combatTimerDuration;
UUID uuid = player.getUniqueId();
@@ -127,6 +145,18 @@ public class FightTagController implements Listener {
return this.config.settings.ignoredWorlds.contains(worldName);
}
private boolean cannotBeTagged(Player player) {
if (this.config.admin.excludeAdminsFromCombat && player.hasPermission("eternalcombat.bypass")) {
return true;
}
if (this.config.admin.excludeAdminsFromCombat && player.isOp()) {
return true;
}
return this.config.admin.excludeCreativePlayersFromCombat && player.getGameMode() == GameMode.CREATIVE;
}
}
@@ -1,152 +0,0 @@
package com.eternalcode.combat.fight.death;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import com.eternalcode.commons.scheduler.Scheduler;
import java.time.Duration;
import java.util.UUID;
import org.bukkit.Color;
import org.bukkit.FireworkEffect;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Firework;
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.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.meta.FireworkMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;
public class DeathFlareController implements Listener {
private final PluginConfig pluginConfig;
private final Server server;
private final Scheduler scheduler;
private final NamespacedKey key;
public DeathFlareController(PluginConfig pluginConfig, Server server, Scheduler scheduler, Plugin plugin) {
this.pluginConfig = pluginConfig;
this.server = server;
this.scheduler = scheduler;
this.key = NamespacedKey.fromString("eternalcombat_firework", plugin);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onFightUntagEvent(FightUntagEvent event) {
CauseOfUnTag cause = event.getCause();
if (cause != CauseOfUnTag.DEATH && cause != CauseOfUnTag.DEATH_BY_PLAYER) {
return;
}
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
if (this.pluginConfig.death.firework.inCombat && !this.pluginConfig.death.firework.afterEveryDeath) {
this.spawnFlare(player);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerDeathEventFlare(PlayerDeathEvent event) {
Player player = event.getEntity();
if (this.pluginConfig.death.firework.afterEveryDeath) {
this.spawnFlare(player);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
if (event.getDamager() instanceof Firework firework && firework.getPersistentDataContainer().has(key, PersistentDataType.STRING)) {
event.setCancelled(true);
}
}
private void spawnFlare(Player player) {
Location deathLocation = player.getLocation();
World world = deathLocation.getWorld();
Firework flare = world.spawn(deathLocation, Firework.class);
flare.getPersistentDataContainer().set(key, PersistentDataType.STRING, "true");
FireworkMeta meta = flare.getFireworkMeta();
Color primaryColor = this.decodeColor(this.pluginConfig.death.firework.primaryColor, "primary");
Color fadeColor = this.decodeColor(this.pluginConfig.death.firework.fadeColor, "fade");
FireworkEffect effect = FireworkEffect.builder()
.with(this.pluginConfig.death.firework.fireworkType)
.withColor(primaryColor)
.withFade(fadeColor)
.trail(true)
.flicker(true)
.build();
meta.addEffect(effect);
meta.setPower(this.pluginConfig.death.firework.power);
flare.setFireworkMeta(meta);
if (this.pluginConfig.death.firework.particlesEnabled) {
scheduleParticles(flare, world);
}
}
private void scheduleParticles(Firework flare, World world) {
this.scheduler.runLaterAsync(
() -> {
if (flare.isDead() || !flare.isValid()) {
return;
}
this.spawnParticles(world, flare);
this.scheduleParticles(flare, world);
}, Duration.ofMillis(50)
);
}
private Color decodeColor(String firework, String name) {
try {
return Color.fromRGB(Integer.decode(firework));
}
catch (NumberFormatException exception) {
throw new IllegalArgumentException(
"Invalid " + name + " format in plugin configuration" + firework,
exception
);
}
}
private void spawnParticles(World world, Firework flare) {
Location location = flare.getLocation();
world.spawnParticle(
this.pluginConfig.death.firework.mainParticle.get(),
location,
this.pluginConfig.death.firework.mainParticleCount,
0.05,
0.05,
0.05,
0.01
);
world.spawnParticle(
this.pluginConfig.death.firework.secondaryParticle.get(),
location,
this.pluginConfig.death.firework.secondaryParticleCount,
0.1,
0.1,
0.1,
0.01
);
}
}
@@ -1,56 +0,0 @@
package com.eternalcode.combat.fight.death;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import java.util.UUID;
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.entity.PlayerDeathEvent;
public class DeathLightningController implements Listener {
private final PluginConfig pluginConfig;
private final Server server;
public DeathLightningController(PluginConfig pluginConfig, Server server) {
this.pluginConfig = pluginConfig;
this.server = server;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onFightUntagEvent(FightUntagEvent event) {
CauseOfUnTag cause = event.getCause();
if (cause != CauseOfUnTag.DEATH && cause != CauseOfUnTag.DEATH_BY_PLAYER) {
return;
}
UUID uniqueId = event.getPlayer();
Player player = this.server.getPlayer(uniqueId);
if (player == null) {
return;
}
if (this.pluginConfig.death.lightning.inCombat && !this.pluginConfig.death.lightning.afterEveryDeath) {
this.lightningStrike(player);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerDeathEventLightning(PlayerDeathEvent event) {
Player player = event.getEntity();
if (this.pluginConfig.death.lightning.afterEveryDeath) {
lightningStrike(player);
}
}
private void lightningStrike(Player player) {
player.getWorld().strikeLightningEffect(player.getLocation());
}
}
@@ -1,77 +0,0 @@
package com.eternalcode.combat.fight.death;
import com.cryptomorin.xseries.particles.XParticle;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import org.bukkit.FireworkEffect;
public class DeathSettings extends OkaeriConfig {
@Comment({
"Settings related to lightning effect upon death",
"Setting both afterEveryDeath and inCombat to false will disable this feature completely"
})
public LightningSettings lightning = new LightningSettings();
public static class LightningSettings extends OkaeriConfig {
@Comment("Should lightning spawn on every death?")
public boolean afterEveryDeath = false;
@Comment("Should lightning spawn on ONLY deaths in combat?")
public boolean inCombat = true;
}
@Comment({
"Settings for the Arc Raiders style flare (firework)",
"Setting both afterEveryDeath and inCombat to false will disable this feature completely"
})
public FlareSettings firework = new FlareSettings();
public static class FlareSettings extends OkaeriConfig {
@Comment("Should firework (flare) spawn on every death?")
public boolean afterEveryDeath = false;
@Comment("Should firework (flare) spawn on ONLY deaths in combat?")
public boolean inCombat = false;
@Comment("Power of firework - how long till explosion (please enter positive number)")
public int power = 2;
@Comment({
"The firework (flare) effect type (BALL, BALL_LARGE, STAR, BURST, CREEPER)",
"Reference: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/FireworkEffect.Type.html"
})
public FireworkEffect.Type fireworkType = FireworkEffect.Type.BALL;
@Comment("Hex color for the firework (flare)")
public String primaryColor = "#a80022";
@Comment("Hex color for the fade of firework (flare)")
public String fadeColor = "#0a0a0a";
@Comment("Toggle on/off additional particles spawned in the firework (flare) path")
public boolean particlesEnabled = true;
@Comment({
"The main trail particle (e.g., CAMPFIRE_COSY_SMOKE, SMOKE_LARGE)",
"Reference: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Particle.html"
})
public XParticle mainParticle = XParticle.CAMPFIRE_COSY_SMOKE;
@Comment("Count of main particles spawned on each tick")
public int mainParticleCount = 3;
@Comment({
"The secondary trail particle (e.g., CAMPFIRE_COSY_SMOKE, SMOKE_LARGE)",
"Reference: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Particle.html"
})
public XParticle secondaryParticle = XParticle.SMALL_FLAME;
@Comment("Count of secondary particles spawned on each tick")
public int secondaryParticleCount = 3;
}
}
@@ -4,6 +4,7 @@ import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -49,11 +50,9 @@ public class FightEffectController implements Listener {
}
if (event.getCause() == CauseOfUnTag.LOGOUT) {
Player player = this.server.getPlayer(event.getPlayer());
if (player == null) {
return;
}
Player player = Bukkit.getPlayer(event.getPlayer());
assert player != null;
this.effectService.clearStoredEffects(player);
}
}
@@ -81,11 +80,9 @@ public class FightEffectController implements Listener {
return;
}
if (event.getCause() == CauseOfUnTag.DEATH_BY_PLAYER || event.getCause() == CauseOfUnTag.DEATH) {
Player player = this.server.getPlayer(event.getPlayer());
if (player == null) {
return;
}
Player player = Bukkit.getPlayer(event.getPlayer());
assert player != null;
this.effectService.clearStoredEffects(player);
}
}
@@ -1,11 +1,12 @@
package com.eternalcode.combat.fight.effect;
import com.cryptomorin.xseries.XPotion;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.util.Map;
import org.bukkit.potion.PotionEffectType;
import java.util.Map;
public class FightEffectSettings extends OkaeriConfig {
@Comment({"# Do you want to add effects to players in combat?"})
@@ -15,20 +16,11 @@ public class FightEffectSettings extends OkaeriConfig {
"# If the option above is set to true, you can add effects to players in combat below",
"# You can find a list of all potion effects here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/potion/PotionEffectType.html",
"# Correct format: 'EFFECT_TYPE:AMPLIFIER' Amplifier strength starts from 0, so level 1 gives effect strength 2",
"# Example: SPEED:1, RESISTANCE:0",
"# Example: SPEED:1, DAMAGE_RESISTANCE:0",
})
public Map<PotionEffectType, Integer> customEffects = Map.of(
requirePotionEffectType(XPotion.SPEED), 1,
requirePotionEffectType(XPotion.RESISTANCE), 0
PotionEffectType.SPEED, 1,
PotionEffectType.DAMAGE_RESISTANCE, 0
);
private static PotionEffectType requirePotionEffectType(XPotion potion) {
PotionEffectType potionEffectType = potion.get();
if (potionEffectType == null) {
throw new IllegalStateException("Potion effect " + potion.name() + " is not supported by this server version");
}
return potionEffectType;
}
}
@@ -1,129 +0,0 @@
package com.eternalcode.combat.fight.knockback;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.region.RegionProvider;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityEvent;
import org.bukkit.plugin.Plugin;
// EntityMountEvent moved between org.spigotmc, io.papermc.paper and org.bukkit across versions,
// so we resolve it at runtime to stay compatible without bumping the compile API.
public final class KnockbackMountController implements Listener {
private static final String[] EVENT_CLASS_CANDIDATES = {
"org.bukkit.event.entity.EntityMountEvent",
"io.papermc.paper.event.entity.EntityMountEvent",
"org.spigotmc.event.entity.EntityMountEvent",
};
private final NoticeService noticeService;
private final RegionProvider regionProvider;
private final FightManager fightManager;
private final AtomicBoolean mountFailureLogged = new AtomicBoolean();
public KnockbackMountController(NoticeService noticeService, RegionProvider regionProvider, FightManager fightManager) {
this.noticeService = noticeService;
this.regionProvider = regionProvider;
this.fightManager = fightManager;
}
public void register(Plugin plugin) {
Class<? extends Event> eventClass = null;
for (String candidate : EVENT_CLASS_CANDIDATES) {
try {
eventClass = Class.forName(candidate).asSubclass(Event.class);
break;
} catch (ClassNotFoundException | ClassCastException ignored) {
}
}
if (eventClass == null) {
plugin.getLogger().warning("Could not find any EntityMountEvent class; mount protection in regions is disabled.");
return;
}
MethodHandle getMountHandle;
try {
getMountHandle = this.resolveGetMountHandle(eventClass);
} catch (NoSuchMethodException | IllegalAccessException exception) {
plugin.getLogger().log(Level.WARNING, "EntityMountEvent has incompatible getMount() method; mount protection in regions is disabled.", exception);
return;
}
plugin.getServer().getPluginManager().registerEvent(
eventClass,
this,
EventPriority.HIGHEST,
(listener, event) -> this.handle(event, getMountHandle, plugin),
plugin,
true
);
}
private MethodHandle resolveGetMountHandle(Class<? extends Event> eventClass) throws NoSuchMethodException, IllegalAccessException {
if (!EntityEvent.class.isAssignableFrom(eventClass) || !Cancellable.class.isAssignableFrom(eventClass)) {
throw new NoSuchMethodException(eventClass.getName() + " is not a cancellable entity event");
}
Method getMount = eventClass.getMethod("getMount");
if (!Entity.class.isAssignableFrom(getMount.getReturnType())) {
throw new NoSuchMethodException(eventClass.getName() + "#getMount() does not return Entity");
}
return MethodHandles.publicLookup()
.unreflect(getMount)
.asType(MethodType.methodType(Entity.class, Event.class));
}
private void handle(Event event, MethodHandle getMountHandle, Plugin plugin) {
if (!(event instanceof EntityEvent entityEvent) || !(event instanceof Cancellable cancellable)) {
return;
}
if (!(entityEvent.getEntity() instanceof Player player)) {
return;
}
if (!this.fightManager.isInCombat(player.getUniqueId())) {
return;
}
Entity mount;
try {
mount = (Entity) getMountHandle.invokeExact(event);
} catch (Throwable exception) {
this.logMountFailure(plugin, exception);
return;
}
if (mount == null || !this.regionProvider.isInRegion(mount.getLocation())) {
return;
}
cancellable.setCancelled(true);
this.noticeService.create()
.player(player.getUniqueId())
.notice(config -> config.messagesSettings.cantEnterOnRegion)
.send();
}
private void logMountFailure(Plugin plugin, Throwable exception) {
if (!this.mountFailureLogged.compareAndSet(false, true)) {
return;
}
plugin.getLogger().log(Level.WARNING, "Could not read EntityMountEvent mount; mount protection in regions is skipped.", exception);
}
}
@@ -3,21 +3,53 @@ package com.eternalcode.combat.fight.knockback;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import org.bukkit.Location;
class KnockbackOutsideRegionGenerator {
static Optional<Location> generate(Point2D min, Point2D max, Location playerLocation, Function<Location, Optional<Location>> safeMapper) {
return generatePoints(min, max, Point2D.from(playerLocation)).entrySet().stream()
.flatMap(entry -> entry.getValue().stream())
.map(point2D -> point2D.toLocation(playerLocation))
.flatMap(safeMapper.andThen(validPoint -> validPoint.stream()))
.findFirst();
static Location generate(Point2D min, Point2D max, Location playerLocation) {
NavigableMap<Double, List<Point2D>> points = generatePoints(min, max, Point2D.from(playerLocation));
NavigableMap<Double, Double> distances = new TreeMap<>();
double totalWeight = 0;
Double maxDistance = points.lastKey();
for (double distance : points.keySet()) {
double weight = createWeight(distance, maxDistance);
distances.put(distance, weight);
totalWeight += weight;
}
double rand = Math.random() * totalWeight;
double cumulativeWeight = 0;
for (Map.Entry<Double, Double> entry : distances.entrySet()) {
double distance = entry.getKey();
double weight = entry.getValue();
cumulativeWeight += weight;
if (rand <= cumulativeWeight) {
return getRandom(points.get(distance))
.toLocation(playerLocation);
}
}
return getRandom(points.firstEntry().getValue())
.toLocation(playerLocation);
}
private static Point2D getRandom(List<Point2D> points) {
return points.get((int) (Math.random() * points.size()));
}
private static double createWeight(double distance, double maxDistance) {
double last = Math.log(maxDistance);
double weight = last - Math.log(distance);
return Math.pow(weight, 10);
}
private static NavigableMap<Double, List<Point2D>> generatePoints(Point2D min, Point2D max, Point2D location) {
@@ -46,4 +78,5 @@ class KnockbackOutsideRegionGenerator {
return Math.sqrt(Math.pow(p2.x() - p1.x(), 2) + Math.pow(p2.z() - p1.z(), 2));
}
}
@@ -9,14 +9,12 @@ import java.time.Duration;
import java.util.Optional;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.entity.Entity;
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;
import org.bukkit.event.vehicle.VehicleMoveEvent;
public class KnockbackRegionController implements Listener {
@@ -91,46 +89,6 @@ public class KnockbackRegionController implements Listener {
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
void onVehicleMove(VehicleMoveEvent event) {
Location locationTo = event.getTo();
Location locationFrom = event.getFrom();
if (locationTo.getBlockX() == locationFrom.getBlockX()
&& locationTo.getBlockY() == locationFrom.getBlockY()
&& locationTo.getBlockZ() == locationFrom.getBlockZ()) {
return;
}
for (Entity passenger : event.getVehicle().getPassengers()) {
if (!(passenger instanceof Player player)) {
continue;
}
if (!this.fightManager.isInCombat(player.getUniqueId())) {
continue;
}
Optional<Region> regionOptional = this.regionProvider.getRegion(locationTo);
if (regionOptional.isEmpty()) {
return;
}
Region region = regionOptional.get();
if (region.contains(locationFrom)) {
this.knockbackService.knockback(region, player);
this.knockbackService.forceKnockbackLater(player, region);
} else {
this.knockbackService.knockbackLater(region, player, Duration.ofMillis(50));
}
this.noticeService.create()
.player(player.getUniqueId())
.notice(config -> config.messagesSettings.cantEnterOnRegion)
.send();
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
void onTag(FightTagEvent event) {
Player player = this.server.getPlayer(event.getPlayer());
@@ -1,29 +1,23 @@
package com.eternalcode.combat.fight.knockback;
import com.cryptomorin.xseries.XMaterial;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.region.Point;
import com.eternalcode.combat.region.Region;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler;
import io.papermc.lib.PaperLib;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.util.Vector;
public final class KnockbackService {
private static final int FORCE_TELEPORT_MAX_ATTEMPTS = 10;
private static final double KNOCKBACK_DAMPEN_FACTOR = 0.8;
public static final int DEPTH_OF_SEARCHING = 10;
private final PluginConfig config;
private final MinecraftScheduler scheduler;
private final RegionProvider regionProvider;
@@ -37,141 +31,50 @@ public final class KnockbackService {
}
public void knockbackLater(Region region, Player player, Duration duration) {
scheduler.runLater(() -> this.knockback(region, player), duration);
}
public void knockback(Region region, Player player) {
if (player.isInsideVehicle()) {
player.leaveVehicle();
}
Location currentLocation = player.getLocation();
Vector shortestDirection = getShortestDirectionToEdge(region, currentLocation);
// Reduces player's current velocity BEFORE applying knockback. Helps create a smoother and more consistent knockback.
player.setVelocity(player.getVelocity().multiply(KNOCKBACK_DAMPEN_FACTOR));
double y = Math.min(player.getVelocity().getY(), config.knockback.vertical);
Vector velocity = shortestDirection.multiply(config.knockback.multiplier).setY(y);
player.setFallDistance(0);
player.setVelocity(velocity);
}
private Vector getShortestDirectionToEdge(Region region, Location current) {
double directionToBottomEdge = current.getX() - region.getMin().getX();
double directionToTopEdge = region.getMax().getX() - current.getX();
double directionToLeftEdge = current.getZ() - region.getMin().getZ();
double directionToRightEdge = region.getMax().getZ() - current.getZ();
double shortestDirection = Math.min(
Math.min(directionToBottomEdge, directionToTopEdge),
Math.min(directionToLeftEdge, directionToRightEdge)
);
if (shortestDirection == directionToBottomEdge)
return new Vector(-1, 0, 0);
if (shortestDirection == directionToTopEdge)
return new Vector(1, 0, 0);
if (shortestDirection == directionToLeftEdge)
return new Vector(0, 0, -1);
if (shortestDirection == directionToRightEdge)
return new Vector(0, 0, 1);
throw new IllegalStateException("Failed to determine direction to edge");
this.scheduler.runLater(() -> this.knockback(region, player), duration);
}
public void forceKnockbackLater(Player player, Region region) {
UUID playerId = player.getUniqueId();
if (insideRegion.containsKey(playerId)) {
if (insideRegion.containsKey(player.getUniqueId())) {
return;
}
insideRegion.put(playerId, region);
insideRegion.put(player.getUniqueId(), region);
scheduler.runLater(player.getLocation(), () -> {
insideRegion.remove(playerId);
scheduler.runLater(
player.getLocation(), () -> {
insideRegion.remove(player.getUniqueId());
Location playerLocation = player.getLocation();
if (!region.contains(playerLocation) && !regionProvider.isInRegion(playerLocation)) {
return;
}
Location playerLocation = player.getLocation();
Location location =
generate(playerLocation, Point2D.from(region.getMin()), Point2D.from(region.getMax()));
if (!region.contains(playerLocation) && !regionProvider.isInRegion(playerLocation)) {
return;
}
if (player.isInsideVehicle()) {
player.leaveVehicle();
}
generate(playerLocation, Point2D.from(region.getMin()), Point2D.from(region.getMax()), 0)
.ifPresent(location -> player.teleportAsync(location, TeleportCause.PLUGIN));
}, config.knockback.forceTeleport.delay);
PaperLib.teleportAsync(player, location, TeleportCause.PLUGIN);
}, this.config.knockback.forceDelay);
}
private Optional<Location> generate(Location playerLocation, Point2D min, Point2D max, int attempts) {
if (attempts >= FORCE_TELEPORT_MAX_ATTEMPTS) {
return Optional.of(playerLocation);
}
Optional<Location> location = KnockbackOutsideRegionGenerator.generate(min, max, playerLocation, candidate -> makeSafe(candidate));
if (location.isEmpty()) {
int expand = (attempts + 1) * (attempts + 1);
return generate(playerLocation, min.expandMin(expand, expand), max.expandMax(expand, expand), attempts + 1);
}
Optional<Region> otherRegion = regionProvider.getRegion(location.get());
private Location generate(Location playerLocation, Point2D minX, Point2D maxX) {
Location location = KnockbackOutsideRegionGenerator.generate(minX, maxX, playerLocation);
Optional<Region> otherRegion = regionProvider.getRegion(location);
if (otherRegion.isPresent()) {
Region region = otherRegion.get();
return generate(playerLocation, min.min(region.getMin()), max.max(region.getMax()), attempts + 1);
return generate(playerLocation, minX.min(region.getMin()), maxX.max(region.getMax()));
}
return location.map(loc -> loc.add(0.5, 0, 0.5));
return location;
}
private Optional<Location> makeSafe(Location random) {
Location maybeSafe = random.clone();
int minY = maybeSafe.getBlockY() - DEPTH_OF_SEARCHING;
public void knockback(Region region, Player player) {
Point point = region.getCenter();
Location subtract = player.getLocation().subtract(point.x(), 0, point.z());
CachedPillarOfBlocks pillar = new CachedPillarOfBlocks(maybeSafe.getBlockX(), maybeSafe.getBlockZ(), maybeSafe.getWorld());
Vector knockbackVector = new Vector(subtract.getX(), 0, subtract.getZ()).normalize();
double multiplier = this.config.knockback.multiplier;
Vector configuredVector = new Vector(multiplier, 0.5, multiplier);
for (int y = maybeSafe.getBlockY(); y > minY; y--) {
maybeSafe.setY(y);
Material ground = pillar.get(maybeSafe.getBlockY() - 1);
Material legs = pillar.get(maybeSafe.getBlockY());
Material head = pillar.get(maybeSafe.getBlockY() + 1);
if (ground.isSolid()
&& !containsMaterial(config.knockback.forceTeleport.unsafeGroundBlocks, ground)
&& containsMaterial(config.knockback.forceTeleport.airBlocks, legs)
&& containsMaterial(config.knockback.forceTeleport.airBlocks, head)
) {
return Optional.of(maybeSafe);
}
}
return Optional.empty();
player.setVelocity(knockbackVector.multiply(configuredVector));
}
private boolean containsMaterial(Set<XMaterial> materials, Material material) {
return materials.contains(XMaterial.matchXMaterial(material));
}
private static class CachedPillarOfBlocks {
private final int x;
private final int z;
private final World world;
private final Map<Integer, Material> blocksTypes = new HashMap<>();
private CachedPillarOfBlocks(int x, int z, World world) {
this.x = x;
this.z = z;
this.world = world;
}
public Material get(int y) {
return this.blocksTypes.computeIfAbsent(y, __ -> world.getType(x, y, z));
}
}
}
@@ -1,138 +1,19 @@
package com.eternalcode.combat.fight.knockback;
import com.cryptomorin.xseries.XMaterial;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
import java.util.EnumSet;
import java.util.Set;
public class KnockbackSettings extends OkaeriConfig {
@Comment({
"# Horizontal knockback strength multiplier.",
"# This controls how far the player is pushed horizontally.",
"#",
"# Applied in: direction.multiply(multiplier)",
"# Example: 1.0 ≈ 2–4 blocks push"
"# Adjust the knockback multiplier for restricted regions.",
"# Higher values increase the knockback distance. Avoid using negative values.",
"# A value of 1.0 typically knocks players 2-4 blocks away."
})
public double multiplier = 1.0;
@Comment({
"# Vertical velocity applied when the player is considered on ground.",
"#",
"# Used when Y velocity is near zero (< 0.08).",
"# Higher values = more upward knockback.",
"# Recommended: 0.15 - 0.3"
})
public double vertical = 0.2;
@Comment({
"# Force teleport settings for players that fail to be knocked back outside the region.",
"# If a player cannot be knocked back outside the region after multiple attempts,",
"# they will be teleported to a safe location outside the region after a short delay.",
"# This ensures players don't get stuck inside the region due to knockback failures."
})
public ForceTeleport forceTeleport = new ForceTeleport();
public static class ForceTeleport extends OkaeriConfig {
@Comment({
"# Delay before force teleport is applied after entering a region.",
"# Recommended: 1-3 seconds to allow knockback attempts before teleporting.",
})
public Duration delay = Duration.ofSeconds(1);
@Comment({
"# Blocks that are considered unsafe to stand on when searching for ground.",
"# These blocks will be ignored during safe ground detection.",
"#",
"# Used in findSafeGround():",
"# - Skips these blocks even if they are solid",
"# - Prevents teleporting onto dangerous or invalid blocks",
"#",
"# Examples:",
"# - BARRIER (invisible collision)",
"# - LAVA / WATER (damage / movement issues)",
"# - CACTUS / MAGMA (damage blocks)",
})
public Set<XMaterial> unsafeGroundBlocks = EnumSet.of(
XMaterial.BARRIER,
XMaterial.LAVA,
XMaterial.WATER,
XMaterial.CACTUS,
XMaterial.MAGMA_BLOCK,
XMaterial.FIRE,
XMaterial.SOUL_FIRE,
XMaterial.COBWEB,
XMaterial.SWEET_BERRY_BUSH,
XMaterial.BEDROCK,
XMaterial.TNT,
XMaterial.SEAGRASS,
XMaterial.TALL_SEAGRASS,
XMaterial.BUBBLE_COLUMN,
XMaterial.POWDER_SNOW,
XMaterial.WITHER_ROSE
);
@Comment({
"# Safe blocks that players can be teleported into",
"# These blocks don't cause damage and allow free movement",
"# Players can safely spawn in these blocks or pass through them",
"# Includes air, grass, flowers, and other non-solid blocks"
})
public Set<XMaterial> airBlocks = EnumSet.of(
XMaterial.AIR,
XMaterial.CAVE_AIR,
XMaterial.VOID_AIR,
XMaterial.TALL_SEAGRASS,
XMaterial.SEAGRASS,
XMaterial.SHORT_GRASS,
XMaterial.TALL_GRASS,
XMaterial.VINE,
XMaterial.STRUCTURE_VOID,
XMaterial.DEAD_BUSH,
XMaterial.DANDELION,
XMaterial.POPPY,
XMaterial.BLUE_ORCHID,
XMaterial.ALLIUM,
XMaterial.AZURE_BLUET,
XMaterial.RED_TULIP,
XMaterial.ORANGE_TULIP,
XMaterial.WHITE_TULIP,
XMaterial.PINK_TULIP,
XMaterial.OXEYE_DAISY,
XMaterial.CORNFLOWER,
XMaterial.LILY_OF_THE_VALLEY,
XMaterial.SUNFLOWER,
XMaterial.LILAC,
XMaterial.ROSE_BUSH,
XMaterial.PEONY,
XMaterial.WITHER_ROSE,
XMaterial.LARGE_FERN,
XMaterial.RAIL,
XMaterial.POWERED_RAIL,
XMaterial.DETECTOR_RAIL,
XMaterial.ACTIVATOR_RAIL,
XMaterial.REDSTONE_WIRE,
XMaterial.COMPARATOR,
XMaterial.REPEATER,
XMaterial.LEVER,
XMaterial.STRING,
XMaterial.SNOW,
XMaterial.CRIMSON_ROOTS,
XMaterial.WARPED_ROOTS,
XMaterial.CRIMSON_FUNGUS,
XMaterial.WARPED_FUNGUS,
XMaterial.TORCH,
XMaterial.WALL_TORCH,
XMaterial.REDSTONE_TORCH,
XMaterial.REDSTONE_WALL_TORCH,
XMaterial.SOUL_TORCH,
XMaterial.SOUL_WALL_TORCH
);
}
public double multiplier = 1;
@Comment({ "# Time after which the player will be force knocked back outside the safe zone" })
public Duration forceDelay = Duration.ofSeconds(1);
}
@@ -5,13 +5,11 @@ import org.bukkit.World;
record Point2D(int x, int z) {
Location toLocation(Location playerLocation) {
World world = playerLocation.getWorld();
int y = world.getEnvironment() == World.Environment.NETHER
? playerLocation.getBlockY() + 1
: world.getHighestBlockYAt(x, z) + 1;
Location toLocation(Location location) {
World world = location.getWorld();
int y = world.getHighestBlockYAt(x, z) + 1;
return new Location(world, x, y, z, playerLocation.getYaw(), playerLocation.getPitch());
return new Location(world, x, y, z, location.getYaw(), location.getPitch());
}
static Point2D from(Location location) {
@@ -26,12 +24,4 @@ record Point2D(int x, int z) {
return new Point2D(Math.max(this.x, max.getBlockX()), Math.max(this.z, max.getBlockZ()));
}
public Point2D expandMax(int x, int z) {
return new Point2D(this.x + x, this.z + z);
}
public Point2D expandMin(int x, int z) {
return new Point2D(this.x - x, this.z - z);
}
}
@@ -2,10 +2,12 @@ package com.eternalcode.combat.fight.pearl;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.time.Duration;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.entity.EnderPearl;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -14,26 +16,30 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
public class PearlController implements Listener {
public class FightPearlController implements Listener {
private final PluginConfig pluginConfig;
private final PearlService pearlService;
private final FightPearlSettings settings;
private final NoticeService noticeService;
private final FightManager fightManager;
private final FightPearlService fightPearlService;
private final PluginConfig config;
public PearlController(
PluginConfig pluginConfig,
PearlService pearlService, NoticeService noticeService, FightManager fightManager
public FightPearlController(
FightPearlSettings settings,
NoticeService noticeService,
FightManager fightManager,
FightPearlService fightPearlService,
PluginConfig config
) {
this.pluginConfig = pluginConfig;
this.pearlService = pearlService;
this.settings = settings;
this.noticeService = noticeService;
this.fightManager = fightManager;
this.fightPearlService = fightPearlService;
this.config = config;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@EventHandler(priority = EventPriority.HIGHEST)
public void onPearlThrow(ProjectileLaunchEvent event) {
if (!(event.getEntity() instanceof EnderPearl)) {
return;
@@ -45,68 +51,63 @@ public class PearlController implements Listener {
UUID playerId = player.getUniqueId();
if (this.pearlService.shouldCancelEvent(playerId)) {
event.setCancelled(true);
if (this.pluginConfig.pearl.pearlThrowDisabledDuringCombat) {
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.pearl.pearlThrowBlockedDuringCombat)
.send();
return;
}
Duration remainingDelay = this.pearlService.getRemainingDelay(playerId);
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.pearl.pearlThrowBlockedDelayDuringCombat)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.send();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
this.pearlService.handleDelay(player);
if (this.settings.pearlThrowDisabledDuringCombat) {
event.setCancelled(true);
this.noticeService.create()
.player(playerId)
.notice(this.settings.pearlThrowBlockedDuringCombat)
.send();
return;
}
if (this.settings.pearlCooldownEnabled) {
handlePearlCooldown(event, player, playerId);
}
if (this.settings.pearlResetsTimer) {
Duration combatTime = this.config.settings.combatTimerDuration;
this.fightManager.tag(playerId, combatTime, CauseOfTag.NON_PLAYER);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPearlDamage(EntityDamageByEntityEvent event) {
if (this.pluginConfig.pearl.pearlThrowDamageEnabled) {
if (this.settings.pearlThrowDamageEnabled) {
return;
}
if (!(event.getEntity() instanceof Player) ||
!(event.getDamager() instanceof EnderPearl) ||
event.getCause() != EntityDamageEvent.DamageCause.PROJECTILE) {
event.getCause() != EntityDamageEvent.DamageCause.FALL) {
return;
}
event.setDamage(0.0);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPearlTeleport(PlayerTeleportEvent event) {
if (event.getCause() != PlayerTeleportEvent.TeleportCause.ENDER_PEARL) {
private void handlePearlCooldown(ProjectileLaunchEvent event, Player player, UUID playerId) {
if (this.settings.pearlThrowDelay.isZero()) {
return;
}
if (!this.pluginConfig.pearl.pearlThrowDisabledDuringCombat) {
if (this.fightPearlService.hasDelay(playerId)) {
event.setCancelled(true);
Duration remainingDelay = this.fightPearlService.getRemainingDelay(playerId);
this.noticeService.create()
.player(playerId)
.notice(this.settings.pearlThrowBlockedDelayDuringCombat)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.send();
return;
}
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
event.setCancelled(true);
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.pearl.pearlThrowBlockedDuringCombat)
.send();
this.fightPearlService.markDelay(playerId);
int cooldownTicks = (int) (this.settings.pearlThrowDelay.toMillis() / 50);
player.setCooldown(Material.ENDER_PEARL, cooldownTicks);
}
}
@@ -0,0 +1,50 @@
package com.eternalcode.combat.fight.pearl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
public class FightPearlServiceImpl implements FightPearlService {
private final FightPearlSettings pearlSettings;
private final Cache<UUID, Instant> pearlStartTimes;
public FightPearlServiceImpl(FightPearlSettings pearlSettings) {
this.pearlSettings = pearlSettings;
this.pearlStartTimes = Caffeine.newBuilder()
.expireAfterWrite(pearlSettings.pearlThrowDelay)
.build();
}
@Override
public void markDelay(UUID uuid) {
this.pearlStartTimes.put(uuid, Instant.now());
}
@Override
public boolean hasDelay(UUID uuid) {
return this.pearlStartTimes.getIfPresent(uuid) != null;
}
@Override
public Duration getRemainingDelay(UUID uuid) {
Instant startTime = this.pearlStartTimes.getIfPresent(uuid);
if (startTime == null) {
return Duration.ZERO;
}
Duration elapsed = Duration.between(startTime, Instant.now());
Duration remaining = this.pearlSettings.pearlThrowDelay.minus(elapsed);
return remaining.isNegative() ? Duration.ZERO : remaining;
}
@Override
public Instant getDelay(UUID uuid) {
Instant startTime = this.pearlStartTimes.getIfPresent(uuid);
return startTime != null ? startTime.plus(this.pearlSettings.pearlThrowDelay) : Instant.MIN;
}
}
@@ -1,12 +1,13 @@
package com.eternalcode.combat.fight.pearl;
import com.eternalcode.multification.bukkit.notice.BukkitNotice;
import com.eternalcode.multification.notice.Notice;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
public class PearlSettings extends OkaeriConfig {
public class FightPearlSettings extends OkaeriConfig {
@Comment({ "# Is pearl damage to be enabled?", "# This will work globally" })
public boolean pearlThrowDamageEnabled = true;
@@ -21,7 +22,7 @@ public class PearlSettings extends OkaeriConfig {
public boolean pearlCooldownEnabled = false;
@Comment("# Set true, If you want to reset timer when player throws ender pearl")
public boolean pearlExtendsCombatTag = false;
public boolean pearlResetsTimer = true;
@Comment({
"# Block throwing pearls with delay?",
@@ -30,12 +31,12 @@ public class PearlSettings extends OkaeriConfig {
public Duration pearlThrowDelay = Duration.ofSeconds(3);
@Comment("# Message sent when player tries to throw ender pearl, but are disabled")
public Notice pearlThrowBlockedDuringCombat = Notice.builder()
public Notice pearlThrowBlockedDuringCombat = BukkitNotice.builder()
.chat("<red>Throwing ender pearls is prohibited during combat!")
.build();
@Comment("# Message sent when player tries to throw ender pearl, but has delay")
public Notice pearlThrowBlockedDelayDuringCombat = Notice.builder()
public Notice pearlThrowBlockedDelayDuringCombat = BukkitNotice.builder()
.chat("<red>You must wait {TIME} before next throw!")
.build();
@@ -1,81 +0,0 @@
package com.eternalcode.combat.fight.pearl;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.commons.delay.Delay;
import com.eternalcode.commons.scheduler.Scheduler;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.UUID;
public class PearlServiceImpl implements PearlService {
private final FightManager fightManager;
private final PluginConfig pluginConfig;
private final Scheduler scheduler;
private final Delay<UUID> pearlStartTimes;
public PearlServiceImpl(FightManager fightManager, PluginConfig pluginConfig, Scheduler scheduler) {
this.fightManager = fightManager;
this.pluginConfig = pluginConfig;
this.pearlStartTimes = Delay.withDefault(() -> pluginConfig.pearl.pearlThrowDelay);
this.scheduler = scheduler;
}
@Override
public boolean shouldCancelEvent(UUID playerId) {
if (fightManager.isInCombat(playerId)) {
if (this.pluginConfig.pearl.pearlCooldownEnabled) {
return this.pearlStartTimes.hasDelay(playerId);
}
return pluginConfig.pearl.pearlThrowDisabledDuringCombat;
}
return false;
}
@Override
public void handleDelay(Player player) {
UUID uniqueId = player.getUniqueId();
if (this.hasDelay(uniqueId)) {
return;
}
if (this.fightManager.isInCombat(uniqueId)) {
if (this.pluginConfig.pearl.pearlExtendsCombatTag) {
Duration combatTime = this.pluginConfig.settings.combatTimerDuration;
this.fightManager.tag(uniqueId, combatTime, CauseOfTag.ENDER_PEARL);
}
if (this.pluginConfig.pearl.pearlCooldownEnabled && !this.pluginConfig.pearl.pearlThrowDelay.isZero()) {
this.pearlStartTimes.markDelay(uniqueId);
this.scheduler.runLater(
() -> player.setCooldown(
Material.ENDER_PEARL,
(int) this.pluginConfig.pearl.pearlThrowDelay.toMillis() / 50
), Duration.ofMillis(50)
);
}
}
}
@Override
public boolean hasDelay(UUID uuid) {
return this.pearlStartTimes.hasDelay(uuid);
}
@Override
public Duration getRemainingDelay(UUID uuid) {
return this.pearlStartTimes.getRemaining(uuid);
}
}
@@ -0,0 +1,107 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import org.bukkit.Material;
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.PlayerInteractEvent;
import org.bukkit.event.player.PlayerRiptideEvent;
import org.bukkit.inventory.ItemStack;
import java.time.Duration;
import java.util.UUID;
public class FightTridentController implements Listener {
private final FightTridentSettings settings;
private final NoticeService noticeService;
private final FightManager fightManager;
private final FightTridentService fightTridentService;
private final PluginConfig config;
public FightTridentController(
FightTridentSettings settings,
NoticeService noticeService,
FightManager fightManager,
FightTridentService fightTridentService,
PluginConfig config
) {
this.settings = settings;
this.noticeService = noticeService;
this.fightManager = fightManager;
this.fightTridentService = fightTridentService;
this.config = config;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onTridentInteract(PlayerInteractEvent event) {
ItemStack item = event.getItem();
if (item == null || item.getType() != Material.TRIDENT) {
return;
}
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
if (this.settings.tridentThrowDisabledDuringCombat) {
event.setCancelled(true);
this.noticeService.create()
.player(playerId)
.notice(this.settings.tridentThrowBlockedDuringCombat)
.send();
return;
}
if (this.settings.tridentCooldownEnabled) {
this.handleTridentCooldown(event, player, playerId);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onTridentRiptide(PlayerRiptideEvent event) {
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
if (this.settings.tridentResetsTimer) {
Duration combatTime = this.config.settings.combatTimerDuration;
this.fightManager.tag(playerId, combatTime, CauseOfTag.NON_PLAYER);
}
}
private void handleTridentCooldown(PlayerInteractEvent event, Player player, UUID playerId) {
if (this.settings.tridentThrowDelay.isZero()) {
return;
}
if (this.fightTridentService.hasDelay(playerId)) {
event.setCancelled(true);
Duration remainingDelay = this.fightTridentService.getRemainingDelay(playerId);
this.noticeService.create()
.player(playerId)
.notice(this.settings.tridentThrowBlockedDelayDuringCombat)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.send();
return;
}
this.fightTridentService.markDelay(playerId);
int cooldownTicks = (int) (this.settings.tridentThrowDelay.toMillis() / 50);
player.setCooldown(Material.TRIDENT, cooldownTicks);
}
}
@@ -0,0 +1,51 @@
package com.eternalcode.combat.fight.trident;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
public class FightTridentServiceImpl implements FightTridentService {
private final FightTridentSettings tridentSettings;
private final Cache<@NotNull UUID, Instant> tridentStartTimes;
public FightTridentServiceImpl(FightTridentSettings tridentSettings) {
this.tridentSettings = tridentSettings;
this.tridentStartTimes = Caffeine.newBuilder()
.expireAfterWrite(tridentSettings.tridentThrowDelay)
.build();
}
@Override
public void markDelay(UUID uuid) {
this.tridentStartTimes.put(uuid, Instant.now());
}
@Override
public boolean hasDelay(UUID uuid) {
return this.tridentStartTimes.getIfPresent(uuid) != null;
}
@Override
public Duration getRemainingDelay(UUID uuid) {
Instant startTime = this.tridentStartTimes.getIfPresent(uuid);
if (startTime == null) {
return Duration.ZERO;
}
Duration elapsed = Duration.between(startTime, Instant.now());
Duration remaining = this.tridentSettings.tridentThrowDelay.minus(elapsed);
return remaining.isNegative() ? Duration.ZERO : remaining;
}
@Override
public Instant getDelay(UUID uuid) {
Instant startTime = this.tridentStartTimes.getIfPresent(uuid);
return startTime != null ? startTime.plus(this.tridentSettings.tridentThrowDelay) : Instant.MIN;
}
}
@@ -0,0 +1,39 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.multification.bukkit.notice.BukkitNotice;
import com.eternalcode.multification.notice.Notice;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
public class FightTridentSettings extends OkaeriConfig {
@Comment({
"# Set to true to disable throwing tridents during combat.",
"# This will work globally, but can be overridden by region settings"
})
public boolean tridentThrowDisabledDuringCombat = false;
@Comment("# Set to true so throwing trident will result in cooldown - delay between throws")
public boolean tridentCooldownEnabled = false;
@Comment("# Set to true, so the users will get combat log when they throw tridents")
public boolean tridentResetsTimer = false;
@Comment({
"# Should throwing trident be on cooldown?",
"# Setting this option to 3s will make players wait 3 seconds between trident throws"
})
public Duration tridentThrowDelay = Duration.ofSeconds(3);
@Comment("# Message sent to the player when throwing trident is disabled")
public Notice tridentThrowBlockedDuringCombat = BukkitNotice.builder()
.chat("<red>Throwing trident is prohibited during combat!")
.build();
@Comment("# Message marking delay between trident throws")
public Notice tridentThrowBlockedDelayDuringCombat = BukkitNotice.builder()
.chat("<red>You must wait {TIME} before next throw!")
.build();
}
@@ -1,146 +0,0 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.time.Duration;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.enchantments.Enchantment;
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.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerRiptideEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
public class TridentController implements Listener {
private final PluginConfig pluginConfig;
private final NoticeService noticeService;
private final FightManager fightManager;
private final TridentService tridentService;
private final Server server;
public TridentController(
PluginConfig pluginConfig,
NoticeService noticeService,
FightManager fightManager,
TridentService tridentService, Server server
) {
this.pluginConfig = pluginConfig;
this.noticeService = noticeService;
this.fightManager = fightManager;
this.tridentService = tridentService;
this.server = server;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onRiptideInteract(PlayerInteractEvent event) {
if (!this.isRiptideInteract(event)) {
return;
}
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
if (this.pluginConfig.trident.tridentRiptideDisabledDuringCombat) {
event.setCancelled(true);
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.trident.tridentRiptideBlocked)
.send();
return;
}
if (!this.tridentService.hasDelay(playerId)) {
return;
}
event.setCancelled(true);
}
// we need to send message without ignoring canceled event because of riptide cooldown
@EventHandler(priority = EventPriority.LOWEST)
public void messageOnRiptideCooldown(PlayerInteractEvent event) {
if (!this.isRiptideInteract(event)) {
return;
}
UUID playerId = event.getPlayer().getUniqueId();
if (!this.tridentService.hasDelay(playerId)) {
return;
}
Duration remainingDelay = this.tridentService.getRemainingDelay(playerId);
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.trident.tridentRiptideOnCooldown)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.send();
}
@EventHandler(ignoreCancelled = true)
public void onRiptide(PlayerRiptideEvent event) {
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (this.pluginConfig.trident.tridentRiptideDisabledDuringCombat) {
return;
}
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
if (this.tridentService.hasDelay(uniqueId)) {
return;
}
this.tridentService.markDelay(player.getUniqueId());
player.setCooldown(Material.TRIDENT, (int) this.pluginConfig.trident.tridentRiptideDelay.toMillis() / 50);
if (this.pluginConfig.trident.tridentRiptideExtendsCombatTag) {
this.fightManager.tag(uniqueId, this.pluginConfig.settings.combatTimerDuration, CauseOfTag.TRIDENT);
}
}
@EventHandler(ignoreCancelled = true)
public void onUntag(FightUntagEvent event) {
Player player = server.getPlayer(event.getPlayer());
player.setCooldown(Material.TRIDENT, 0);
this.tridentService.removeDelay(player.getUniqueId());
}
private boolean isRiptideInteract(PlayerInteractEvent event) {
EquipmentSlot hand = event.getHand();
if (hand != EquipmentSlot.HAND && hand != EquipmentSlot.OFF_HAND) {
return false;
}
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) {
return false;
}
return this.isRiptideTrident(event.getItem());
}
private boolean isRiptideTrident(ItemStack itemStack) {
return itemStack != null
&& itemStack.getType() == Material.TRIDENT
&& itemStack.containsEnchantment(Enchantment.RIPTIDE);
}
}
@@ -1,41 +0,0 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.commons.delay.Delay;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.UUID;
public class TridentServiceImpl implements TridentService {
private final Delay<UUID> delay;
public TridentServiceImpl(PluginConfig pluginConfig) {
this.delay = Delay.withDefault(() -> pluginConfig.trident.tridentRiptideDelay);
}
@Override
public void markDelay(UUID uuid) {
this.delay.markDelay(uuid);
}
@Override
public boolean hasDelay(UUID uuid) {
return this.delay.hasDelay(uuid);
}
@Override
public void removeDelay(UUID playerId) {
this.delay.unmarkDelay(playerId);
}
@Override
public Duration getRemainingDelay(UUID uuid) {
return this.delay.getRemaining(uuid);
}
}
@@ -1,39 +0,0 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.multification.notice.Notice;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
public class TridentSettings extends OkaeriConfig {
@Comment({
"# Set to true to disable riptide usage during combat",
"# This setting works globally, but can be overridden by region settings"
})
public boolean tridentRiptideDisabledDuringCombat = false;
@Comment("# Set to true so then riptide usage during combat will extend combat tag duration, works only if riptide usage is not disabled")
public boolean tridentRiptideExtendsCombatTag = false;
@Comment({
"# Should riptide enchantment be on cooldown during combat?",
"# Setting this option to 3s will make players wait 3 seconds between trident throws",
"# Setting this to 0s or below will remove cooldown"
})
public Duration tridentRiptideDelay = Duration.ofSeconds(10);
@Comment("# Message shown when riptide is blocked during combat")
public Notice tridentRiptideBlocked = Notice.builder()
.chat("<red>Using riptide is prohibited during combat!")
.build();
@Comment({
"# Message sent to the player when riptide is on cooldown",
"# Available placeholder: {TIME} - remaining time left to use riptide again"
})
public Notice tridentRiptideOnCooldown = Notice.builder()
.chat("<red>You must wait {TIME} before next riptide!")
.build();
}
@@ -2,20 +2,24 @@ package com.eternalcode.combat.notification;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.multification.adventure.AudienceConverter;
import com.eternalcode.multification.paper.PaperMultification;
import com.eternalcode.multification.bukkit.BukkitMultification;
import com.eternalcode.multification.translation.TranslationProvider;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.ComponentSerializer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public final class NoticeService extends PaperMultification<PluginConfig> {
public final class NoticeService extends BukkitMultification<PluginConfig> {
private final AudienceProvider audienceProvider;
private final PluginConfig pluginConfig;
private final MiniMessage miniMessage;
public NoticeService(PluginConfig pluginConfig, MiniMessage miniMessage) {
public NoticeService(AudienceProvider audienceProvider, PluginConfig pluginConfig, MiniMessage miniMessage) {
this.audienceProvider = audienceProvider;
this.pluginConfig = pluginConfig;
this.miniMessage = miniMessage;
}
@@ -32,7 +36,13 @@ public final class NoticeService extends PaperMultification<PluginConfig> {
@Override
protected @NotNull AudienceConverter<CommandSender> audienceConverter() {
return commandSender -> commandSender;
return commandSender -> {
if (commandSender instanceof Player player) {
return this.audienceProvider.player(player.getUniqueId());
}
return this.audienceProvider.console();
};
}
}
@@ -8,14 +8,13 @@ import org.bukkit.Location;
import org.bukkit.World;
record WorldGuardRegion(World world, ProtectedRegion region) implements Region {
@Override
public Point getCenter() {
BlockVector3 min = this.region.getMinimumPoint();
BlockVector3 max = this.region.getMaximumPoint();
double x = (min.x() + max.x()) / 2.0;
double z = (min.z() + max.z()) / 2.0;
double x = (double) (min.getX() + max.getX()) / 2;
double z = (double) (min.getZ() + max.getZ()) / 2;
return new Point(this.world, x, z);
}
@@ -23,13 +22,12 @@ record WorldGuardRegion(World world, ProtectedRegion region) implements Region {
@Override
public Location getMin() {
BlockVector3 min = this.region.getMinimumPoint();
return new Location(this.world, min.x(), min.y(), min.z());
return new Location(this.world, min.getX(), min.getY(), min.getZ());
}
@Override
public Location getMax() {
BlockVector3 max = this.region.getMaximumPoint();
return new Location(this.world, max.x(), max.y(), max.z());
return new Location(this.world, max.getX(), max.getY(), max.getZ());
}
}
@@ -2,6 +2,8 @@ package com.eternalcode.combat.updater;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.commons.concurrent.FutureHandler;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -14,17 +16,20 @@ public class UpdaterNotificationController implements Listener {
private final UpdaterService updaterService;
private final PluginConfig pluginConfig;
private final AudienceProvider audienceProvider;
private final MiniMessage miniMessage;
public UpdaterNotificationController(UpdaterService updaterService, PluginConfig pluginConfig, MiniMessage miniMessage) {
public UpdaterNotificationController(UpdaterService updaterService, PluginConfig pluginConfig, AudienceProvider audienceProvider, MiniMessage miniMessage) {
this.updaterService = updaterService;
this.pluginConfig = pluginConfig;
this.audienceProvider = audienceProvider;
this.miniMessage = miniMessage;
}
@EventHandler
void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Audience audience = this.audienceProvider.player(player.getUniqueId());
if (!shouldNotify(player)) {
return;
@@ -33,7 +38,7 @@ public class UpdaterNotificationController implements Listener {
this.updaterService.checkForUpdate()
.thenAccept(result -> {
if (result.isUpdateAvailable()) {
player.sendMessage(this.miniMessage.deserialize(NEW_VERSION_AVAILABLE));
audience.sendMessage(this.miniMessage.deserialize(NEW_VERSION_AVAILABLE));
}
})
.exceptionally(FutureHandler::handleException);
@@ -0,0 +1,27 @@
package com.eternalcode.combat.util;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class ReflectUtil {
private ReflectUtil() {
}
@SuppressWarnings("unchecked")
public static <T> T invokeMethod(Object object, String name) {
try {
if (object == null) {
return null;
}
Method method = object.getClass().getDeclaredMethod(name);
method.setAccessible(true);
return (T) method.invoke(object);
}
catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException exception) {
throw new RuntimeException(exception);
}
}
}
@@ -1,15 +0,0 @@
package com.eternalcode.combat.config.implementation;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;
class SignEditingSettingsTest {
@Test
void shouldDisableSignEditingBlockerByDefault() {
SignEditingSettings signEditingSettings = new SignEditingSettings();
assertFalse(signEditingSettings.disableSignEditingDuringCombat);
}
}
@@ -1,34 +0,0 @@
package com.eternalcode.combat.fight.blocker;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
class SignEditingBlockerTest {
@Test
void shouldRecognizeStandingSignMaterials() {
assertTrue(SignEditingBlocker.isSign(Material.OAK_SIGN));
assertTrue(SignEditingBlocker.isSign(Material.CRIMSON_SIGN));
}
@Test
void shouldRecognizeWallSignMaterials() {
assertTrue(SignEditingBlocker.isSign(Material.OAK_WALL_SIGN));
assertTrue(SignEditingBlocker.isSign(Material.WARPED_WALL_SIGN));
}
@Test
void shouldIgnoreNonSignMaterials() {
assertFalse(SignEditingBlocker.isSign(Material.CHEST));
assertFalse(SignEditingBlocker.isSign(Material.ENDER_PEARL));
}
@Test
void shouldRecognizeEnderPearlMaterial() {
assertTrue(SignEditingBlocker.isEnderPearl(Material.ENDER_PEARL));
assertFalse(SignEditingBlocker.isEnderPearl(Material.OAK_SIGN));
}
}
-3
View File
@@ -2,6 +2,3 @@ org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.parallel=true
# Platforms
paperVersion=1.19.3,1.19.4,1.20,1.20.1,1.20.2,1.20.4,1.20.5,1.20.6,1.21,1.21.1,1.21.3,1.21.4,1.21.5,1.21.6,1.21.7,1.21.8,1.21.9,1.21.10,1.21.11,26.1.1,26.1.2
Binary file not shown.
+1 -3
View File
@@ -1,9 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+3 -3
View File
@@ -20,7 +20,7 @@
##############################################################################
#
# gradlew start up script for POSIX generated by Gradle.
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh gradlew
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
Vendored
+23 -12
View File
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,18 +65,29 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
goto fail
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
File renamed without changes.