Compare commits
196
Commits
1.2.7
...
old/java17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac34ba181d | ||
|
|
66d58dd71c | ||
|
|
c861a13d5e | ||
|
|
6b3b7c5e6f | ||
|
|
a5f2cc5378 | ||
|
|
9dd167219a | ||
|
|
5491ba198b | ||
|
|
991cea235a | ||
|
|
19fefc2fd3 | ||
|
|
24a50f7239 | ||
|
|
7489f7024b | ||
|
|
4d37ce07af | ||
|
|
d5df3e9ea5 | ||
|
|
66836699f5 | ||
|
|
d11cf4d95e | ||
|
|
ec0db9e578 | ||
|
|
e653f102e3 | ||
|
|
e2dded2988 | ||
|
|
692da7013d | ||
|
|
25dadda7fe | ||
|
|
c0cb1dd35e | ||
|
|
91c4d4fe89 | ||
|
|
f28f6a2231 | ||
|
|
a05f8964ba | ||
|
|
5c195036fe | ||
|
|
13d1dfab51 | ||
|
|
93071afab8 | ||
|
|
122e203e81 | ||
|
|
97b57a6eeb | ||
|
|
12519ec43d | ||
|
|
d34b9bfdf0 | ||
|
|
c9c8abe613 | ||
|
|
da80e5565b | ||
|
|
31589150ca | ||
|
|
4bd46f43d3 | ||
|
|
5fe0e4209d | ||
|
|
88b1b93dc8 | ||
|
|
05ddff2901 | ||
|
|
28954e2fec | ||
|
|
722c099a41 | ||
|
|
6ad1d384ad | ||
|
|
d690d9f697 | ||
|
|
8a4572639d | ||
|
|
7eb0ba635e | ||
|
|
7213f0474b | ||
|
|
c378235c78 | ||
|
|
5aebec5b19 | ||
|
|
f141b14cbf | ||
|
|
082be5390e | ||
|
|
1e616a47b7 | ||
|
|
2df423c9a6 | ||
|
|
5d17900144 | ||
|
|
0a1d6a88c4 | ||
|
|
c05c39387c | ||
|
|
27f21b816d | ||
|
|
2cab200f4d | ||
|
|
fd5f859c04 | ||
|
|
15b8958252 | ||
|
|
a3b3495f9a | ||
|
|
1e7faa0a39 | ||
|
|
b2bf8bb654 | ||
|
|
f06b9fbf52 | ||
|
|
663225bd13 | ||
|
|
478763b3e0 | ||
|
|
96587a91ff | ||
|
|
64e3db8952 | ||
|
|
2804204a26 | ||
|
|
3d08a8e501 | ||
|
|
2b15318159 | ||
|
|
b83298bb0f | ||
|
|
7c176a69bf | ||
|
|
38342d1795 | ||
|
|
0857493c50 | ||
|
|
761ef0ec4f | ||
|
|
dc0923db3c | ||
|
|
a241fa5281 | ||
|
|
d2fe9f2f43 | ||
|
|
41e8989cdb | ||
|
|
444bab6ed5 | ||
|
|
4ce5b0e187 | ||
|
|
b50b5a90d0 | ||
|
|
48c12743cd | ||
|
|
f90c0f0ac8 | ||
|
|
6c79f03102 | ||
|
|
f22ca70ba1 | ||
|
|
78261a60d1 | ||
|
|
bb7f7b4af7 | ||
|
|
39c0d2866a | ||
|
|
d8a1b77106 | ||
|
|
6b86a858a1 | ||
|
|
092626652e | ||
|
|
c533dc50ca | ||
|
|
3e6d45d78e | ||
|
|
41aec83da2 | ||
|
|
c642ea5387 | ||
|
|
14a24e0129 | ||
|
|
795a3f9549 | ||
|
|
930474d1f3 | ||
|
|
44a5721fc2 | ||
|
|
a00cb0bbd8 | ||
|
|
8c5cda208b | ||
|
|
897c0b3174 | ||
|
|
20112e7cc3 | ||
|
|
ceb37adc4d | ||
|
|
05eae887a6 | ||
|
|
ecd0971808 | ||
|
|
ffd46b421e | ||
|
|
33186385df | ||
|
|
161ee44626 | ||
|
|
dbbfa673ec | ||
|
|
1d62a23035 | ||
|
|
19a6256fac | ||
|
|
1a4d43ada7 | ||
|
|
a06fb205c5 | ||
|
|
107d00bf1b | ||
|
|
ce05e6727d | ||
|
|
5d8149628b | ||
|
|
f7b883bc51 | ||
|
|
604d0ba2e9 | ||
|
|
55762d9b0f | ||
|
|
08327a5e75 | ||
|
|
4bc2b14453 | ||
|
|
a505aef267 | ||
|
|
88ae48e69f | ||
|
|
018b253fc8 | ||
|
|
bba9af7c23 | ||
|
|
af44b07061 | ||
|
|
fd11719a8a | ||
|
|
93458ac4a5 | ||
|
|
dd37267f85 | ||
|
|
e58992e6e8 | ||
|
|
3cc55ae08d | ||
|
|
0f1c438bac | ||
|
|
c7bc803682 | ||
|
|
f20c13959e | ||
|
|
dac5b41c01 | ||
|
|
cc04f58d01 | ||
|
|
d8bd77cb23 | ||
|
|
746f873d6c | ||
|
|
596a8b9738 | ||
|
|
b786236f46 | ||
|
|
18f0bd2ab3 | ||
|
|
eeae5a9937 | ||
|
|
5959a4a4db | ||
|
|
5e60dc6577 | ||
|
|
46c2b2bfcf | ||
|
|
7a0ff7b7fd | ||
|
|
79b1ba09b7 | ||
|
|
98ee6e6fbe | ||
|
|
aa2e8e5b38 | ||
|
|
88b51c921e | ||
|
|
ab00fef477 | ||
|
|
045cf3ff72 | ||
|
|
6176c0e532 | ||
|
|
5358487f93 | ||
|
|
097524abd3 | ||
|
|
a26ddd8979 | ||
|
|
ba5ab34226 | ||
|
|
4373d3fbfe | ||
|
|
f054f65c27 | ||
|
|
3dc4ad8515 | ||
|
|
c18d85a7d3 | ||
|
|
5bc426d1aa | ||
|
|
414715e52e | ||
|
|
7b546f0112 | ||
|
|
abc1f4091b | ||
|
|
656e056f6f | ||
|
|
58cec86fa2 | ||
|
|
feb060d09b | ||
|
|
ef9b53dea0 | ||
|
|
c71dd4ec8e | ||
|
|
4ec0118cdb | ||
|
|
dbb491728c | ||
|
|
e1d8b3b198 | ||
|
|
8eb882f536 | ||
|
|
68aef32524 | ||
|
|
ec7adff6cd | ||
|
|
0c97dc8db3 | ||
|
|
1daa11f48e | ||
|
|
7f36f7a98c | ||
|
|
0ec1f30d93 | ||
|
|
2db7845a55 | ||
|
|
56599759d1 | ||
|
|
9bf2e5fdbf | ||
|
|
9ecd1ce60c | ||
|
|
32fe2798cb | ||
|
|
2c7f39ba91 | ||
|
|
d00673cffd | ||
|
|
9b60ce8bbb | ||
|
|
59645420ee | ||
|
|
fdad7512ba | ||
|
|
2f62d7cf73 | ||
|
|
111bf3ddee | ||
|
|
27201c7217 | ||
|
|
951b72f205 | ||
|
|
8aa578f44f |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
github: [Eisi05]
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
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.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,93 @@
|
||||
name: Bug Report
|
||||
description: Report an issue with NpcAPI to help us improve
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please search existing issues to avoid creating duplicates.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: What's the issue you're experiencing?
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: server_software
|
||||
attributes:
|
||||
label: Server Software
|
||||
description: Select one option
|
||||
options:
|
||||
- label: Spigot
|
||||
- label: Paper
|
||||
- label: Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: other_server_software
|
||||
attributes:
|
||||
label: Please specify server software
|
||||
description: Required if 'Other' is selected above
|
||||
placeholder: e.g., Purpur, Tuinity, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: server_version
|
||||
attributes:
|
||||
label: Server Version
|
||||
description: e.g., 1.21.11
|
||||
placeholder: 1.x.x
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: plugin_version
|
||||
attributes:
|
||||
label: Plugin/API Version
|
||||
description: Use /npc version
|
||||
placeholder: e.g., 1.7.5 or 1.4.5-SNAPSHOT
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: usage_type
|
||||
attributes:
|
||||
label: Using API/Plugin
|
||||
options:
|
||||
- label: Using NpcAPI as a dependency
|
||||
- label: Using NpcAPI as a plugin
|
||||
- label: Both
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please provide clear steps to reproduce the issue.
|
||||
1. First step
|
||||
2. Second step
|
||||
3. And so on...
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen?
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What actually happened?
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error Logs
|
||||
description: Paste any error logs here. It might be helpful to run `/npc config debug true` and `/npc config reload` to see an error message in the console.
|
||||
render: text
|
||||
- type: textarea
|
||||
id: media
|
||||
attributes:
|
||||
label: Screenshots/Videos
|
||||
description: If applicable, add links to screenshots or videos to help explain your problem.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement for NpcAPI
|
||||
labels: [enhancement, needs-triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please search existing issues to avoid duplicates.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: A clear and concise description of the feature or improvement you're suggesting.
|
||||
placeholder: What would you like to see added or improved?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: use_case
|
||||
attributes:
|
||||
label: Use Case
|
||||
description: Explain how this feature would be used and what problem it solves.
|
||||
- type: checkboxes
|
||||
id: server_software
|
||||
attributes:
|
||||
label: Server Software
|
||||
options:
|
||||
- label: Spigot
|
||||
- label: Paper
|
||||
- label: Other (please specify)
|
||||
- type: input
|
||||
id: server_version
|
||||
attributes:
|
||||
label: Server Version
|
||||
description: e.g., 1.21.11
|
||||
placeholder: 1.x.x
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
@@ -1,74 +1,46 @@
|
||||
name: Notify Discord on New Issue
|
||||
name: Notify Discord on New Comment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
notify_comment:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Prepare Embed Data
|
||||
id: prep
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
// Truncate description to 1024
|
||||
let body = issue.body || 'No description provided.';
|
||||
if (body.length > 1024) body = body.substring(0, 1021) + '...';
|
||||
|
||||
body = "\n" + body + "\n";
|
||||
|
||||
// Escape JSON special characters
|
||||
function escapeJSON(str) {
|
||||
return str.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n');
|
||||
}
|
||||
|
||||
const labels = issue.labels || [];
|
||||
let color = 5814783; // default blue
|
||||
let labelsText = labels.length > 0 ? labels.map(l => l.name).join(', ') : 'Label missing';
|
||||
|
||||
core.setOutput('title', escapeJSON(issue.title || 'No Title'));
|
||||
core.setOutput('description', escapeJSON(body));
|
||||
core.setOutput('color', color);
|
||||
core.setOutput('labels', escapeJSON(labelsText));
|
||||
|
||||
- name: Send Discord embed
|
||||
env:
|
||||
TITLE: ${{ steps.prep.outputs.title }}
|
||||
DESCRIPTION: ${{ steps.prep.outputs.description }}
|
||||
COLOR: ${{ steps.prep.outputs.color }}
|
||||
LABELS: ${{ steps.prep.outputs.labels }}
|
||||
ISSUE_URL: ${{ github.event.issue.html_url }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
AUTHOR_NAME: ${{ github.event.issue.user.login }}
|
||||
AUTHOR_URL: ${{ github.event.issue.user.html_url }}
|
||||
AUTHOR_ICON: ${{ github.event.issue.user.avatar_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
TIMESTAMP: ${{ github.event.issue.created_at }}
|
||||
run: |
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"embeds\": [{
|
||||
\"title\": \"🆕 New Issue #${ISSUE_NUMBER}: ${TITLE}\",
|
||||
\"url\": \"${ISSUE_URL}\",
|
||||
\"description\": \"${DESCRIPTION}\",
|
||||
\"color\": ${COLOR},
|
||||
\"fields\": [
|
||||
{\"name\": \"Labels\", \"value\": \"${LABELS}\", \"inline\": true},
|
||||
{\"name\": \"State\", \"value\": \"Open\", \"inline\": true}
|
||||
],
|
||||
\"author\": {
|
||||
\"name\": \"${AUTHOR_NAME}\",
|
||||
\"url\": \"${AUTHOR_URL}\",
|
||||
\"icon_url\": \"${AUTHOR_ICON}\"
|
||||
},
|
||||
\"footer\": {\"text\": \"GitHub → ${REPO}\"},
|
||||
\"timestamp\": \"${TIMESTAMP}\"
|
||||
}]
|
||||
}" \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
- name: Send Discord embed
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
let body = issue.body || "No description provided.";
|
||||
if (body.length > 1024) body = body.substring(0,1021) + "...";
|
||||
|
||||
const payload = {
|
||||
embeds: [{
|
||||
title: `🆕 New Issue #${issue.number}: ${issue.title}`,
|
||||
url: issue.html_url,
|
||||
description: body,
|
||||
color: 5814783,
|
||||
fields: [
|
||||
{ name: "State", value: "Open", inline: true }
|
||||
],
|
||||
author: {
|
||||
name: issue.user.login,
|
||||
url: issue.user.html_url,
|
||||
icon_url: issue.user.avatar_url
|
||||
},
|
||||
footer: { text: `GitHub → ${context.repo.owner}/${context.repo.repo}` },
|
||||
timestamp: issue.created_at
|
||||
}]
|
||||
};
|
||||
|
||||
await fetch(process.env.DISCORD_WEBHOOK, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
@@ -7,10 +7,7 @@ on:
|
||||
jobs:
|
||||
notify_comment:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# ⬇️ ADD THIS CONDITION TO THE *JOB*
|
||||
# This skips the *entire job* if the commenter is the user you want to ignore.
|
||||
# Replace 'YOUR-USERNAME-TO-IGNORE' with your actual GitHub login.
|
||||
|
||||
if: github.event.comment.user.login != 'Eisi05'
|
||||
|
||||
steps:
|
||||
@@ -22,15 +19,9 @@ jobs:
|
||||
const comment = context.payload.comment;
|
||||
const issue = context.payload.issue;
|
||||
|
||||
// ⬇️ REMOVE THIS LINE - the job-level 'if' handles this now.
|
||||
// if (comment.user.login === github.actor) return;
|
||||
|
||||
// 1. Comment body (truncate to 1024)
|
||||
let body = comment.body || 'No content provided.';
|
||||
if (body.length > 1024) body = body.substring(0, 1021) + '...';
|
||||
|
||||
// ... (rest of your script is good) ...
|
||||
|
||||
body = "\n" + body + "\n";
|
||||
function escapeJSON(str) {
|
||||
return str.replace(/\\/g, '\\\\')
|
||||
@@ -45,7 +36,6 @@ jobs:
|
||||
core.setOutput('title', escapeJSON(`Comment on #${issue.number}: ${issue.title}`));
|
||||
core.setOutput('description', body);
|
||||
core.setOutput('color', color);
|
||||
// ... (all your other core.setOutput lines) ...
|
||||
core.setOutput('labels', escapeJSON(labelsText));
|
||||
core.setOutput('issue_url', issue.html_url);
|
||||
core.setOutput('comment_author', comment.user.login);
|
||||
@@ -54,26 +44,32 @@ jobs:
|
||||
core.setOutput('timestamp', comment.created_at);
|
||||
|
||||
- name: Send Discord embed
|
||||
# ⬇️ REMOVE THIS 'if' - it's no longer needed.
|
||||
# if: steps.prep.outputs.description != ''
|
||||
run: |
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"embeds\": [{
|
||||
\"title\": \"${{ steps.prep.outputs.title }}\",
|
||||
\"url\": \"${{ steps.prep.outputs.issue_url }}\",
|
||||
\"description\": \"${{ steps.prep.outputs.description }}\",
|
||||
\"color\": ${{ steps.prep.outputs.color }},
|
||||
\"fields\": [
|
||||
{\"name\": \"Labels\", \"value\": \"${{ steps.prep.outputs.labels }}\", \"inline\": true},
|
||||
{\"name\": \"State\", \"value\": \"Open\", \"inline\": true}
|
||||
],
|
||||
\"author\": {
|
||||
\"name\": \"${{ steps.prep.outputs.comment_author }}\",
|
||||
\"url\": \"${{ steps.prep.outputs.comment_author_url }}\",
|
||||
\"icon_url\": \"${{ steps.prep.outputs.comment_author_icon }}\"
|
||||
},
|
||||
\"timestamp\": \"${{ steps.prep.outputs.timestamp }}\"
|
||||
}]
|
||||
}" \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const payload = {
|
||||
embeds: [{
|
||||
title: `${{ steps.prep.outputs.title }}`,
|
||||
url: `${{ steps.prep.outputs.issue_url }}`,
|
||||
description: `${{ steps.prep.outputs.description }}`,
|
||||
color: Number(`${{ steps.prep.outputs.color }}`),
|
||||
fields: [
|
||||
{ name: "Labels", value: `${{ steps.prep.outputs.labels }}`, inline: true },
|
||||
{ name: "State", value: "Open", inline: true }
|
||||
],
|
||||
author: {
|
||||
name: `${{ steps.prep.outputs.comment_author }}`,
|
||||
url: `${{ steps.prep.outputs.comment_author_url }}`,
|
||||
icon_url: `${{ steps.prep.outputs.comment_author_icon }}`
|
||||
},
|
||||
timestamp: `${{ steps.prep.outputs.timestamp }}`
|
||||
}]
|
||||
};
|
||||
|
||||
await fetch(process.env.WEBHOOK, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
@@ -1,3 +1,5 @@
|
||||
download_wiki.ps1
|
||||
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
A powerful and easy-to-use NPC (Non-Player Character) API for Minecraft Spigot plugins that allows you to create, manage, and customize NPCs with
|
||||
advanced features.
|
||||
|
||||
## Wiki
|
||||
|
||||
See the in-repo wiki pages under:
|
||||
[📚 Project Wiki](https://github.com/Eisi05/NpcApi-Spigot/wiki)
|
||||
|
||||
## Features
|
||||
|
||||
- Create custom NPCs with ease
|
||||
@@ -39,7 +44,7 @@ This method requires [NpcPlugin](https://www.spigotmc.org/resources/npc-plugin-1
|
||||
<dependency>
|
||||
<groupId>com.github.Eisi05</groupId>
|
||||
<artifactId>NpcApi</artifactId>
|
||||
<version>1.2.6</version>
|
||||
<version>2.3.3</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
@@ -55,7 +60,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.github.Eisi05:NpcApi:1.2.6'
|
||||
compileOnly 'com.github.Eisi05:NpcApi:2.3.3'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -88,7 +93,7 @@ Add the repository and dependency to your `pom.xml`:
|
||||
<dependency>
|
||||
<groupId>com.github.Eisi05</groupId>
|
||||
<artifactId>NpcApi</artifactId>
|
||||
<version>1.2.6</version>
|
||||
<version>2.3.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
@@ -105,7 +110,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.Eisi05:NpcApi:1.2.6'
|
||||
implementation 'com.github.Eisi05:NpcApi:2.3.3'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -220,7 +225,7 @@ NPC npc = NpcManager.fromUUID(npcUuid);
|
||||
| `hideNpcFromPlayer(Player)` | Hide NPC from specific player |
|
||||
| `lookAtPlayer(Player)` | Make NPC look at player |
|
||||
| `delete()` | Remove NPC permanently |
|
||||
| `walkTo(Path, player, double, boolean, Consumer<Result>)` | Let the NPC walk along a path (can be created with PathfindingUtils class) |
|
||||
| `walkTo(Path, double, boolean, Consumer<Result>, players)` | Let the NPC walk along a path (can be created with PathfindingUtils class) |
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.eisi05</groupId>
|
||||
<artifactId>NpcApi</artifactId>
|
||||
<version>1.2.7</version>
|
||||
<version>2.3.3</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>NpcApi</name>
|
||||
@@ -103,17 +103,5 @@
|
||||
<version>1.10.8</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.14</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
<version>4.4.16</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,52 +1,55 @@
|
||||
package de.eisi05.npc.api;
|
||||
|
||||
import de.eisi05.npc.api.listeners.ChangeWorldListener;
|
||||
import de.eisi05.npc.api.listeners.ConnectionListener;
|
||||
import de.eisi05.npc.api.listeners.NpcInteractListener;
|
||||
import de.eisi05.npc.api.listeners.*;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcConfig;
|
||||
import de.eisi05.npc.api.objects.NpcHolder;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import de.eisi05.npc.api.scheduler.Tasks;
|
||||
import de.eisi05.npc.api.utils.Metrics;
|
||||
import de.eisi05.npc.api.utils.PacketReader;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedPlayerTeam;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.configuration.serialization.ConfigurationSerialization;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* The main entry point and singleton class for the NPC API.
|
||||
* This class handles the initialization, configuration, and shutdown
|
||||
* of the NPC functionality within a Bukkit plugin.
|
||||
* The main entry point and singleton class for the NPC API. This class handles the initialization, configuration, and shutdown of the NPC functionality within
|
||||
* a Bukkit plugin.
|
||||
*/
|
||||
public final class NpcApi
|
||||
{
|
||||
private static final List<Listener> listeners = new ArrayList<>(List.of(new ChangeWorldListener(), new ConnectionListener(), new NpcInteractListener(),
|
||||
new WorldLoadListener(), new ServerReadyListener(), new ProjectileHitListener()));
|
||||
|
||||
/**
|
||||
* A static reference to the Bukkit plugin instance that is using this API.
|
||||
* This is set during the API's initialization.
|
||||
* A static reference to the Bukkit plugin instance that is using this API. This is set during the API's initialization.
|
||||
*/
|
||||
public static Plugin plugin;
|
||||
|
||||
public static Function<Player, String> DISABLED_MESSAGE_PROVIDER = player -> ChatColor.RED + "DISABLED";
|
||||
|
||||
/**
|
||||
* The configuration object for the NPC API, containing various settings
|
||||
* like the look-at timer.
|
||||
* The configuration object for the NPC API, containing various settings like the look-at timer.
|
||||
*/
|
||||
public static NpcConfig config = new NpcConfig();
|
||||
|
||||
private static Listener sleepListener;
|
||||
private static NpcApi npcApi;
|
||||
|
||||
/**
|
||||
* Private constructor to enforce the singleton pattern.
|
||||
* Initializes the API by registering listeners, loading existing NPCs,
|
||||
* injecting packet readers, and starting recurring tasks.
|
||||
* Private constructor to enforce the singleton pattern. Initializes the API by registering listeners, loading existing NPCs, injecting packet readers, and
|
||||
* starting recurring tasks.
|
||||
*
|
||||
* @param plugin The {@link JavaPlugin} instance using this API. Must not be {@code null}.
|
||||
* @param config The {@link NpcConfig} object for the API. Must not be {@code null}.
|
||||
@@ -56,21 +59,24 @@ public final class NpcApi
|
||||
NpcApi.plugin = plugin;
|
||||
NpcApi.config = config;
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(new ChangeWorldListener(), plugin);
|
||||
Bukkit.getPluginManager().registerEvents(new ConnectionListener(), plugin);
|
||||
Bukkit.getPluginManager().registerEvents(new NpcInteractListener(), plugin);
|
||||
if(config.preciseSleepingHitbox())
|
||||
listeners.add(sleepListener = new NpcSleepListener());
|
||||
|
||||
listeners.forEach(listener -> Bukkit.getPluginManager().registerEvents(listener, plugin));
|
||||
|
||||
ConfigurationSerialization.registerClass(Path.class);
|
||||
|
||||
NpcManager.loadNPCs();
|
||||
PacketReader.injectAll();
|
||||
|
||||
Tasks.start();
|
||||
PacketReader.injectAll();
|
||||
|
||||
new Metrics(plugin, 28179).addCustomChart(new Metrics.SingleLineChart("npcCount", () -> NpcManager.getList().size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or retrieves the singleton instance of the {@code NpcApi} with a default configuration.
|
||||
* If the API instance does not exist or the provided plugin is null, a new instance is created.
|
||||
* Creates or retrieves the singleton instance of the {@code NpcApi} with a default configuration. If the API instance does not exist or the provided plugin
|
||||
* is null, a new instance is created.
|
||||
*
|
||||
* @param plugin The {@link JavaPlugin} instance using this API. Must not be {@code null}.
|
||||
* @return The singleton {@link NpcApi} instance. Must not be {@code null}.
|
||||
@@ -81,8 +87,8 @@ public final class NpcApi
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or retrieves the singleton instance of the {@code NpcApi} with a custom configuration.
|
||||
* If the API instance does not exist or the provided plugin is null, a new instance is created.
|
||||
* Creates or retrieves the singleton instance of the {@code NpcApi} with a custom configuration. If the API instance does not exist or the provided plugin
|
||||
* is null, a new instance is created.
|
||||
*
|
||||
* @param plugin The {@link JavaPlugin} instance using this API. Must not be {@code null}.
|
||||
* @param config The {@link NpcConfig} object to use for the API. Must not be {@code null}.
|
||||
@@ -96,6 +102,51 @@ public final class NpcApi
|
||||
return npcApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the NPC API, performing the necessary cleanup. This includes hiding all active NPCs from players, clearing the NPC manager, and uninjecting
|
||||
* packet readers. It also nullifies the static references.
|
||||
*/
|
||||
public static void disable()
|
||||
{
|
||||
List<NpcHolder> npcsToSave = new ArrayList<>(NpcManager.getList());
|
||||
npcsToSave.stream().filter(NpcHolder::hasUnsavedChanges).parallel().forEach(npc ->
|
||||
{
|
||||
if(npc instanceof NPC npc1 && !npc1.isSaved())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
npc.save();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
}
|
||||
});
|
||||
NpcManager.getList().forEach(NPC::hideNpcFromAllPlayers);
|
||||
NpcManager.clear();
|
||||
PacketReader.uninjectAll();
|
||||
Tasks.stop();
|
||||
WrappedPlayerTeam.clear();
|
||||
ConfigurationSerialization.unregisterClass(Path.class);
|
||||
|
||||
listeners.forEach(HandlerList::unregisterAll);
|
||||
|
||||
NpcManager.loadExceptions.clear();
|
||||
npcApi = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the sleep listener, unregistering the old one and registering a new one.
|
||||
*/
|
||||
public static void reloadSleepListener()
|
||||
{
|
||||
if(sleepListener != null && !config.preciseSleepingHitbox())
|
||||
HandlerList.unregisterAll(sleepListener);
|
||||
|
||||
if(config.preciseSleepingHitbox())
|
||||
Bukkit.getPluginManager().registerEvents(sleepListener = new NpcSleepListener(), plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a function that provides the message shown when an NPC is disabled.
|
||||
*
|
||||
@@ -107,23 +158,4 @@ public final class NpcApi
|
||||
DISABLED_MESSAGE_PROVIDER = function;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the NPC API, performing the necessary cleanup.
|
||||
* This includes hiding all active NPCs from players, clearing the NPC manager,
|
||||
* and uninjecting packet readers. It also nullifies the static references.
|
||||
*/
|
||||
public static void disable()
|
||||
{
|
||||
NpcManager.getList().forEach(NPC::hideNpcFromAllPlayers);
|
||||
NpcManager.clear();
|
||||
PacketReader.uninjectAll();
|
||||
Tasks.stop();
|
||||
WrappedPlayerTeam.clear();
|
||||
ConfigurationSerialization.unregisterClass(Path.class);
|
||||
|
||||
NpcManager.loadExceptions.clear();
|
||||
npcApi = null;
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package de.eisi05.npc.api.ai;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.utils.SerializablePredicate;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Base abstract class for NPC AI goals. Goals represent specific behaviors that an NPC can perform, such as walking to a location, attacking an entity, or
|
||||
* looking around. Extend this class to create custom goal behaviors.
|
||||
*/
|
||||
public abstract class Goal implements Serializable
|
||||
{
|
||||
@Serial
|
||||
private final static long serialVersionUID = 1L;
|
||||
private Priority priority;
|
||||
private SerializablePredicate<NPC> condition;
|
||||
|
||||
/**
|
||||
* Creates a new goal with the specified priority.
|
||||
*
|
||||
* @param priority The priority level for this goal
|
||||
*/
|
||||
protected Goal(@NotNull Priority priority)
|
||||
{
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
private Goal()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the condition for this goal. The condition is checked before the goal is executed.
|
||||
*
|
||||
* @return The condition for this goal
|
||||
*/
|
||||
public @Nullable SerializablePredicate<NPC> getCondition()
|
||||
{
|
||||
return condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the condition for this goal. The condition is checked before the goal is executed.
|
||||
*
|
||||
* @param condition The condition to set
|
||||
*/
|
||||
public void setCondition(@Nullable SerializablePredicate<NPC> condition)
|
||||
{
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the priority of this goal.
|
||||
*
|
||||
* @return The priority of this goal
|
||||
*/
|
||||
public final @NotNull Priority getPriority()
|
||||
{
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the priority of this goal.
|
||||
*
|
||||
* @param priority The new priority for this goal
|
||||
*/
|
||||
public final void setPriority(@NotNull Priority priority)
|
||||
{
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this goal can be used right now. This is called each tick to determine if the goal should be considered for execution.
|
||||
*
|
||||
* @param npc The NPC to check for
|
||||
* @return true if this goal can be used, false otherwise
|
||||
*/
|
||||
protected boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return condition == null || condition.test(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this goal starts executing. Use this to initialize any state needed for the goal.
|
||||
*
|
||||
* @param npc The NPC starting this goal
|
||||
*/
|
||||
protected abstract void start(@NotNull NPC npc);
|
||||
|
||||
/**
|
||||
* Called each tick while this goal is active. Use this to update the goal's behavior.
|
||||
*
|
||||
* @param npc The NPC executing this goal
|
||||
*/
|
||||
protected abstract void tick(@NotNull NPC npc);
|
||||
|
||||
/**
|
||||
* Called when this goal stops executing. Use this to clean up any state or cancel ongoing tasks.
|
||||
*
|
||||
* @param npc The NPC stopping this goal
|
||||
*/
|
||||
protected abstract void stop(@NotNull NPC npc);
|
||||
|
||||
/**
|
||||
* Checks whether this goal can continue executing. If this returns false, the goal will be stopped and a new goal will be selected.
|
||||
*
|
||||
* @param npc The NPC to check for
|
||||
* @return true if this goal can continue, false otherwise
|
||||
*/
|
||||
protected boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return condition == null || condition.test(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this goal can be interrupted by a new goal selection. If this returns true, the goal selector may switch to a different goal even if this
|
||||
* goal can continue. This is useful for goals that have natural break points (e.g., waiting periods).
|
||||
*
|
||||
* @param npc The NPC to check for
|
||||
* @return true if this goal can be interrupted, false otherwise
|
||||
*/
|
||||
protected boolean canBeInterrupted(@NotNull NPC npc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this goal can be removed immediately without being queued.
|
||||
* <p>
|
||||
* If this returns true, the goal can be removed instantly from the goal selector. If this returns false, the goal will be queued for removal and will only
|
||||
* be removed once this method returns true or the goal naturally completes.
|
||||
*
|
||||
* @param npc The NPC to check for
|
||||
* @return true if the goal can be removed immediately, false if it should be queued
|
||||
*/
|
||||
protected boolean canBeRemovedNow(@NotNull NPC npc)
|
||||
{
|
||||
return !canContinue(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @return A copy of this goal
|
||||
*/
|
||||
public abstract @NotNull Goal copy();
|
||||
|
||||
/**
|
||||
* Priority levels for goal selection. Higher priority goals are more likely to be selected. ALWAYS priority goals are always preferred over other goals.
|
||||
* Other priorities use weighted random selection where higher weight = higher chance of being selected.
|
||||
*/
|
||||
public enum Priority implements Serializable
|
||||
{
|
||||
/**
|
||||
* This goal is always preferred over other goals and will be selected deterministically.
|
||||
*/
|
||||
ALWAYS(100),
|
||||
|
||||
/**
|
||||
* Very high chance of being selected when among non-ALWAYS goals.
|
||||
*/
|
||||
HIGHEST(4),
|
||||
|
||||
/**
|
||||
* High chance of being selected when among non-ALWAYS goals.
|
||||
*/
|
||||
HIGH(3),
|
||||
|
||||
/**
|
||||
* Medium chance of being selected when among non-ALWAYS goals.
|
||||
*/
|
||||
MEDIUM(2),
|
||||
|
||||
/**
|
||||
* Low chance of being selected when among non-ALWAYS goals.
|
||||
*/
|
||||
LOW(1);
|
||||
|
||||
@Serial
|
||||
private final static long serialVersionUID = 1L;
|
||||
final int weight;
|
||||
|
||||
Priority(int weight)
|
||||
{
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package de.eisi05.npc.api.ai;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* Manages and selects goals for an NPC. The goal selector evaluates all available goals each tick and selects the highest priority goal that can be used.
|
||||
*/
|
||||
public class GoalSelector
|
||||
{
|
||||
private final NPC npc;
|
||||
private final Set<Goal> removalQueue = new HashSet<>();
|
||||
private Goal currentGoal;
|
||||
private BukkitTask task;
|
||||
private boolean running;
|
||||
private long tickInterval;
|
||||
|
||||
/**
|
||||
* Creates a new GoalSelector for the specified NPC.
|
||||
*
|
||||
* @param npc The NPC to manage goals for
|
||||
*/
|
||||
public GoalSelector(@NotNull NPC npc)
|
||||
{
|
||||
this.npc = npc;
|
||||
this.tickInterval = 1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a goal to this selector.
|
||||
*
|
||||
* @param goal The goal to add
|
||||
* @return This selector for method chaining
|
||||
*/
|
||||
public @NotNull GoalSelector addGoal(@NotNull Goal goal)
|
||||
{
|
||||
npc.addGoal(goal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a goal from this selector.
|
||||
*
|
||||
* @param goal The goal to remove
|
||||
* @return This selector for method chaining
|
||||
*/
|
||||
public @NotNull GoalSelector removeGoal(@NotNull Goal goal)
|
||||
{
|
||||
npc.removeGoal(goal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the goal if it is running
|
||||
*
|
||||
* @param goal The goal to check
|
||||
* @return This selector for method chaining
|
||||
*/
|
||||
public @NotNull GoalSelector stopGoalIfRunning(@NotNull Goal goal)
|
||||
{
|
||||
if(currentGoal == goal)
|
||||
{
|
||||
currentGoal.stop(npc);
|
||||
currentGoal = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a goal for removal. If the goal is currently running, it will be removed immediately if it can be removed now, otherwise it will be queued for
|
||||
* removal and removed after it finishes naturally. If the goal is not currently running, it will be removed immediately from the NPC's goal list.
|
||||
*
|
||||
* @param goal The goal to queue for removal
|
||||
* @return true if the goal was removed immediately or is not currently running, false if the goal was queued for removal
|
||||
*/
|
||||
public boolean queueGoalForRemoval(@NotNull Goal goal)
|
||||
{
|
||||
if(currentGoal == goal)
|
||||
{
|
||||
if(goal.canBeRemovedNow(npc))
|
||||
return true;
|
||||
else
|
||||
{
|
||||
removalQueue.add(goal);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a goal is queued for removal.
|
||||
* <p>
|
||||
* Goals that are currently running cannot be removed immediately and are instead queued for removal once they finish naturally. This method allows checking
|
||||
* whether a specific goal is in that removal queue.
|
||||
*
|
||||
* @param goal The goal to check
|
||||
* @return true if the goal is queued for removal, false otherwise
|
||||
*/
|
||||
public boolean isGoalQueuedForRemoval(@NotNull Goal goal)
|
||||
{
|
||||
return removalQueue.contains(goal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all goals registered with this selector.
|
||||
*
|
||||
* @return A list of all goals
|
||||
*/
|
||||
public @NotNull ArrayList<Goal> getGoals()
|
||||
{
|
||||
return new ArrayList<>(npc.getGoals());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently active goal.
|
||||
*
|
||||
* @return The current goal, or null if no goal is active
|
||||
*/
|
||||
public @Nullable Goal getCurrentGoal()
|
||||
{
|
||||
return currentGoal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the goal selector. This will begin evaluating and executing goals.
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(running)
|
||||
return;
|
||||
|
||||
running = true;
|
||||
task = Bukkit.getScheduler().runTaskTimer(NpcApi.plugin, this::tick, 0L, tickInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the goal selector. This will stop the current goal and cease evaluation.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(!running)
|
||||
return;
|
||||
|
||||
running = false;
|
||||
if(task != null)
|
||||
{
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
|
||||
if(currentGoal != null)
|
||||
{
|
||||
currentGoal.stop(npc);
|
||||
currentGoal = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the goal selector is currently running.
|
||||
*
|
||||
* @return true if running, false otherwise
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return running;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current tick interval.
|
||||
*
|
||||
* @return The tick interval in ticks
|
||||
*/
|
||||
public long getTickInterval()
|
||||
{
|
||||
return tickInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tick interval for goal evaluation.
|
||||
*
|
||||
* @param ticks The number of ticks between evaluations (1 = every tick)
|
||||
*/
|
||||
public void setTickInterval(long ticks)
|
||||
{
|
||||
this.tickInterval = ticks;
|
||||
if(running)
|
||||
{
|
||||
stop();
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually forces a specific goal to start, bypassing normal selection. This is useful for triggering goals based on external events.
|
||||
*
|
||||
* @param goal The goal to force start
|
||||
*/
|
||||
public void forceGoal(@NotNull Goal goal)
|
||||
{
|
||||
if(!getGoals().contains(goal))
|
||||
return;
|
||||
|
||||
if(currentGoal != null)
|
||||
currentGoal.stop(npc);
|
||||
|
||||
currentGoal = goal;
|
||||
currentGoal.start(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the removal queue, removing goals that have finished running.
|
||||
*/
|
||||
private void processRemovalQueue()
|
||||
{
|
||||
if(removalQueue.isEmpty())
|
||||
return;
|
||||
|
||||
List<Goal> copy = new ArrayList<>(removalQueue);
|
||||
for(Goal goal : copy)
|
||||
{
|
||||
if(goal != null && removalQueue.contains(goal) && goal.canBeRemovedNow(npc))
|
||||
{
|
||||
removalQueue.remove(goal);
|
||||
npc.removeGoal(goal);
|
||||
|
||||
if(goal == currentGoal)
|
||||
currentGoal = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main tick method that evaluates and executes goals.
|
||||
*/
|
||||
private void tick()
|
||||
{
|
||||
processRemovalQueue();
|
||||
|
||||
List<Goal> usableGoals = getGoals().stream()
|
||||
.filter(goal -> goal.canUse(npc))
|
||||
.toList();
|
||||
|
||||
if(usableGoals.isEmpty())
|
||||
{
|
||||
if(currentGoal != null)
|
||||
{
|
||||
currentGoal.stop(npc);
|
||||
currentGoal = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
List<Goal> alwaysGoals = usableGoals.stream()
|
||||
.filter(goal -> goal.getPriority() == Goal.Priority.ALWAYS)
|
||||
.toList();
|
||||
|
||||
Goal selectedGoal;
|
||||
if(!alwaysGoals.isEmpty())
|
||||
selectedGoal = alwaysGoals.get(ThreadLocalRandom.current().nextInt(alwaysGoals.size()));
|
||||
else
|
||||
{
|
||||
if(usableGoals.isEmpty())
|
||||
selectedGoal = null;
|
||||
else if(usableGoals.size() == 1)
|
||||
selectedGoal = usableGoals.get(0);
|
||||
else
|
||||
{
|
||||
int totalWeight = usableGoals.stream()
|
||||
.mapToInt(goal -> goal.getPriority().weight)
|
||||
.sum();
|
||||
|
||||
if(totalWeight == 0)
|
||||
selectedGoal = usableGoals.get(ThreadLocalRandom.current().nextInt(usableGoals.size()));
|
||||
else
|
||||
{
|
||||
int randomWeight = ThreadLocalRandom.current().nextInt(totalWeight);
|
||||
int currentWeight = 0;
|
||||
|
||||
selectedGoal = null;
|
||||
for(Goal goal : usableGoals)
|
||||
{
|
||||
currentWeight += goal.getPriority().weight;
|
||||
if(randomWeight < currentWeight)
|
||||
{
|
||||
selectedGoal = goal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(selectedGoal == null)
|
||||
selectedGoal = usableGoals.get(usableGoals.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(currentGoal != null)
|
||||
{
|
||||
if(selectedGoal != null && selectedGoal != currentGoal)
|
||||
{
|
||||
if(selectedGoal.getPriority() == Goal.Priority.ALWAYS || currentGoal.canBeInterrupted(npc))
|
||||
{
|
||||
currentGoal.stop(npc);
|
||||
currentGoal = selectedGoal;
|
||||
currentGoal.start(npc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(currentGoal.canContinue(npc))
|
||||
{
|
||||
currentGoal.tick(npc);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentGoal.stop(npc);
|
||||
currentGoal = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(selectedGoal != null)
|
||||
{
|
||||
currentGoal = selectedGoal;
|
||||
currentGoal.start(npc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,901 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.utils.LocationUtils;
|
||||
import de.eisi05.npc.api.utils.Reflections;
|
||||
import de.eisi05.npc.api.utils.SerializableBiPredicate;
|
||||
import de.eisi05.npc.api.wrapper.enums.Pose;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntityData;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
|
||||
import de.eisi05.npc.api.wrapper.packets.AnimatePacket;
|
||||
import de.eisi05.npc.api.wrapper.packets.PacketWrapper;
|
||||
import de.eisi05.npc.api.wrapper.packets.RemoveEntityPacket;
|
||||
import de.eisi05.npc.api.wrapper.packets.SetEntityDataPacket;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeModifier;
|
||||
import org.bukkit.entity.*;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC attack nearby entities that match a predicate. The attack behavior varies based on the item in the NPC's main hand: -
|
||||
* Bow/Trident/Crossbow: Long range attacks - Sword/Axe: Short range attacks - Other: Short range attacks Only activates when a valid target is in range and
|
||||
* line of sight. Once a target is locked, the NPC continues attacking until the target becomes unreachable or invalid.
|
||||
*/
|
||||
public class AttackEntityGoal extends Goal
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final double WEAPONMECHANICS_ATTACK_RANGE = 25.0;
|
||||
private static final double BOW_ATTACK_RANGE = 15.0;
|
||||
private static final double MELEE_ATTACK_RANGE = 3.0;
|
||||
private static final double LINE_OF_SIGHT_RANGE = 25.0;
|
||||
private static final int BOW_DRAW_DELAY_TICKS = 25;
|
||||
private static final int WEAPONMECHANICS_SHOOT_DELAY_TICKS = 5;
|
||||
private static final double KITING_DISTANCE = 3.0;
|
||||
private static final double OPTIMAL_RANGED_DISTANCE = 6.0;
|
||||
|
||||
private SerializableBiPredicate<LivingEntity, NPC> targetFilter;
|
||||
private double customAttackRange;
|
||||
private double speed;
|
||||
|
||||
private transient LivingEntity target;
|
||||
private transient int attackCooldown;
|
||||
private transient boolean isAttacking;
|
||||
private transient WalkToLocationGoal movementGoal;
|
||||
private transient boolean isUsing;
|
||||
private transient List<Player> cachedViewers;
|
||||
private transient int lineOfSightCheckCooldown;
|
||||
private transient int pathRecalculationCooldown;
|
||||
private transient boolean isKiting;
|
||||
|
||||
/**
|
||||
* Creates an AttackEntityGoal with a filter for valid targets.
|
||||
*
|
||||
* @param targetFilter A predicate to filter valid targets
|
||||
*/
|
||||
public AttackEntityGoal(@NotNull SerializableBiPredicate<LivingEntity, NPC> targetFilter)
|
||||
{
|
||||
this(targetFilter, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an AttackEntityGoal with a filter for valid targets and custom attack range.
|
||||
*
|
||||
* @param targetFilter A predicate to filter valid targets
|
||||
* @param customAttackRange Custom attack range (-1 for default based on weapon)
|
||||
*/
|
||||
public AttackEntityGoal(@NotNull SerializableBiPredicate<LivingEntity, NPC> targetFilter, double customAttackRange)
|
||||
{
|
||||
this(targetFilter, customAttackRange, WalkToLocationGoal.DEFAULT_SPEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an AttackEntityGoal with a filter for valid targets and custom speed.
|
||||
*
|
||||
* @param targetFilter A predicate to filter valid targets
|
||||
* @param speed Custom movement speed (-1 for default)
|
||||
*/
|
||||
public AttackEntityGoal(@NotNull SerializableBiPredicate<LivingEntity, NPC> targetFilter, double customAttackRange, double speed)
|
||||
{
|
||||
super(Priority.ALWAYS);
|
||||
this.targetFilter = targetFilter;
|
||||
this.customAttackRange = customAttackRange;
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private AttackEntityGoal(@NotNull AttackEntityGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.targetFilter = goal.targetFilter;
|
||||
this.customAttackRange = goal.customAttackRange;
|
||||
this.speed = goal.speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target filter for this goal.
|
||||
*
|
||||
* @return the target filter
|
||||
*/
|
||||
public SerializableBiPredicate<LivingEntity, NPC> getTargetFilter()
|
||||
{
|
||||
return targetFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target filter for this goal.
|
||||
*
|
||||
* @param targetFilter the new target filter
|
||||
*/
|
||||
public void setTargetFilter(@NotNull SerializableBiPredicate<LivingEntity, NPC> targetFilter)
|
||||
{
|
||||
this.targetFilter = targetFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom attack range for this goal.
|
||||
*
|
||||
* @return the custom attack range
|
||||
*/
|
||||
public double getCustomAttackRange()
|
||||
{
|
||||
return customAttackRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom attack range for this goal.
|
||||
*
|
||||
* @param customAttackRange the new custom attack range
|
||||
*/
|
||||
public void setCustomAttackRange(double customAttackRange)
|
||||
{
|
||||
this.customAttackRange = customAttackRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed for this goal.
|
||||
*
|
||||
* @return the speed
|
||||
*/
|
||||
public double getSpeed()
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speed for this goal.
|
||||
*
|
||||
* @param speed the new speed
|
||||
*/
|
||||
public void setSpeed(double speed)
|
||||
{
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if a valid target is found or an existing target is still valid
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
if(targetFilter == null)
|
||||
return false;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
if(target != null && target.isValid() && canContinue(npc))
|
||||
return true;
|
||||
|
||||
LivingEntity potentialTarget = findTarget(npc);
|
||||
if(potentialTarget == null)
|
||||
return false;
|
||||
|
||||
this.target = potentialTarget;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the attack goal, initializing all necessary state.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
this.attackCooldown = 0;
|
||||
this.isAttacking = true;
|
||||
movementGoal = null;
|
||||
isUsing = false;
|
||||
lineOfSightCheckCooldown = 0;
|
||||
pathRecalculationCooldown = 0;
|
||||
isKiting = false;
|
||||
setUsingItemState(npc, false);
|
||||
updateCachedViewers(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the attack goal, handling movement, targeting, and attacks.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
if(target == null || !target.isValid())
|
||||
{
|
||||
isAttacking = false;
|
||||
stopMovement(npc);
|
||||
return;
|
||||
}
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
|
||||
if(cachedViewers == null || cachedViewers.size() != npc.getViewers().size())
|
||||
updateCachedViewers(npc);
|
||||
|
||||
for(Player viewer : cachedViewers)
|
||||
npc.lookAtEntity(target, viewer, true);
|
||||
|
||||
if(lineOfSightCheckCooldown > 0)
|
||||
lineOfSightCheckCooldown--;
|
||||
|
||||
double distance = npcLoc.distance(target.getLocation());
|
||||
double attackRange = getAttackRange(npc);
|
||||
|
||||
if(distance > attackRange)
|
||||
{
|
||||
boolean checkLineOfSight = lineOfSightCheckCooldown <= 0;
|
||||
if(checkLineOfSight)
|
||||
lineOfSightCheckCooldown = 5;
|
||||
|
||||
if(distance > LINE_OF_SIGHT_RANGE || (checkLineOfSight && !hasLineOfSight(npc, target)))
|
||||
{
|
||||
stop(npc);
|
||||
return;
|
||||
}
|
||||
|
||||
if(movementGoal == null)
|
||||
startMovement(npc, target.getLocation());
|
||||
else
|
||||
{
|
||||
Location currentTarget = movementGoal.getTargetLocation();
|
||||
boolean shouldRecalculate = currentTarget.distance(target.getLocation()) > 5.0;
|
||||
|
||||
if(!shouldRecalculate && pathRecalculationCooldown <= 0)
|
||||
{
|
||||
shouldRecalculate = true;
|
||||
pathRecalculationCooldown = WalkToLocationGoal.RECALCULATION_COOLDOWN;
|
||||
}
|
||||
|
||||
if(shouldRecalculate)
|
||||
{
|
||||
startMovement(npc, target.getLocation());
|
||||
pathRecalculationCooldown = WalkToLocationGoal.RECALCULATION_COOLDOWN;
|
||||
}
|
||||
else
|
||||
{
|
||||
movementGoal.tick(npc);
|
||||
pathRecalculationCooldown--;
|
||||
}
|
||||
|
||||
distance = npc.getLocation().distance(target.getLocation());
|
||||
if(distance <= attackRange)
|
||||
stopMovement(npc);
|
||||
else
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(isUsingRangedWeapon(npc))
|
||||
{
|
||||
if(distance < KITING_DISTANCE && !isKiting)
|
||||
startKiting(npc);
|
||||
else if(isKiting && distance >= OPTIMAL_RANGED_DISTANCE)
|
||||
stopKiting(npc);
|
||||
else if(isKiting)
|
||||
{
|
||||
if(movementGoal != null)
|
||||
{
|
||||
movementGoal.tick(npc);
|
||||
pathRecalculationCooldown--;
|
||||
if(pathRecalculationCooldown <= 0)
|
||||
{
|
||||
startKiting(npc);
|
||||
pathRecalculationCooldown = WalkToLocationGoal.RECALCULATION_COOLDOWN;
|
||||
}
|
||||
}
|
||||
else
|
||||
isKiting = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
stopMovement(npc);
|
||||
}
|
||||
else
|
||||
stopMovement(npc);
|
||||
|
||||
if(attackCooldown > 0)
|
||||
{
|
||||
attackCooldown--;
|
||||
return;
|
||||
}
|
||||
|
||||
if(distance <= attackRange)
|
||||
{
|
||||
performAttack(npc);
|
||||
attackCooldown = getAttackCooldown(npc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the attack goal and cleans up state.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
stopMovement(npc);
|
||||
target = null;
|
||||
isAttacking = false;
|
||||
attackCooldown = 0;
|
||||
isUsing = false;
|
||||
setUsingItemState(npc, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the target is still valid and in range
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
if(!isAttacking || target == null || !target.isValid())
|
||||
return false;
|
||||
|
||||
if(!super.canContinue(npc))
|
||||
return false;
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = target.getLocation();
|
||||
|
||||
if(!npcLoc.getWorld().equals(targetLoc.getWorld()))
|
||||
return false;
|
||||
|
||||
boolean checkLineOfSight = lineOfSightCheckCooldown <= 0;
|
||||
if(checkLineOfSight && !hasLineOfSight(npc, target))
|
||||
return false;
|
||||
|
||||
double distance = npcLoc.distance(targetLoc);
|
||||
double attackRange = getAttackRange(npc);
|
||||
|
||||
return distance <= Math.max(attackRange, LINE_OF_SIGHT_RANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canBeRemovedNow(@NotNull NPC npc)
|
||||
{
|
||||
if(super.canBeRemovedNow(npc))
|
||||
return true;
|
||||
|
||||
if(movementGoal != null && movementGoal.canContinue(npc))
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location below = npcLoc.clone().subtract(0, 0.1, 0);
|
||||
|
||||
if(below.getBlock().getType().isAir())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new AttackEntityGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a valid target entity near the NPC.
|
||||
*/
|
||||
private LivingEntity findTarget(@NotNull NPC npc)
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
double searchRange = Math.max(getAttackRange(npc), LINE_OF_SIGHT_RANGE);
|
||||
|
||||
for(Entity entity : npcLoc.getWorld().getNearbyEntities(npcLoc, searchRange, searchRange, searchRange))
|
||||
{
|
||||
if(!(entity instanceof LivingEntity livingEntity))
|
||||
continue;
|
||||
|
||||
if(!targetFilter.test(livingEntity, npc))
|
||||
continue;
|
||||
|
||||
if(!hasLineOfSight(npc, livingEntity))
|
||||
continue;
|
||||
|
||||
return livingEntity;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the NPC has line of sight to the target entity.
|
||||
*/
|
||||
private boolean hasLineOfSight(@NotNull NPC npc, @NotNull LivingEntity target)
|
||||
{
|
||||
Location npcLoc = npc.getLocation().clone().add(0, 1.6, 0);
|
||||
Location targetLoc = target.getLocation().clone().add(0, target.getEyeHeight(), 0);
|
||||
|
||||
Vector direction = targetLoc.toVector().subtract(npcLoc.toVector()).normalize();
|
||||
double distance = npcLoc.distance(targetLoc);
|
||||
int steps = (int) (distance * 2);
|
||||
|
||||
for(int i = 0; i < steps; i++)
|
||||
{
|
||||
Location checkLoc = npcLoc.clone().add(direction.clone().multiply(i * 0.5));
|
||||
if(checkLoc.getBlock().getType().isSolid() && !checkLoc.getBlock().isPassable())
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attack range based on the NPC's held item.
|
||||
*/
|
||||
private double getAttackRange(@NotNull NPC npc)
|
||||
{
|
||||
if(customAttackRange > 0)
|
||||
return customAttackRange;
|
||||
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
|
||||
if(mainHand == null)
|
||||
return MELEE_ATTACK_RANGE;
|
||||
|
||||
if(getWeaponTitle(mainHand) != null)
|
||||
return WEAPONMECHANICS_ATTACK_RANGE;
|
||||
|
||||
if(isRangedWeapon(mainHand.getType()))
|
||||
return BOW_ATTACK_RANGE;
|
||||
else
|
||||
return MELEE_ATTACK_RANGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attack cooldown based on the NPC's held item.
|
||||
*/
|
||||
private int getAttackCooldown(@NotNull NPC npc)
|
||||
{
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
|
||||
if(mainHand == null || !mainHand.hasItemMeta())
|
||||
return 5;
|
||||
|
||||
String weaponTitle = getWeaponTitle(mainHand);
|
||||
if(weaponTitle != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Object config = Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanics", "getInstance")
|
||||
.thanInvoke("getWeaponConfigurations").get();
|
||||
if(config != null)
|
||||
{
|
||||
|
||||
int shotsPerSecond = (int) Reflections.invokeMethod(config, "getInt", weaponTitle + ".Shoot.Fully_Automatic_Shots_Per_Second", 0).get();
|
||||
if(shotsPerSecond > 0)
|
||||
return Math.max(1, 20 / shotsPerSecond);
|
||||
|
||||
|
||||
int burstDelay = (int) Reflections.invokeMethod(config, "getInt", weaponTitle + ".Shoot.Burst.Ticks_Between_Each_Shot", 0).get();
|
||||
if(burstDelay > 0)
|
||||
return burstDelay;
|
||||
|
||||
|
||||
int shootDelay = (int) Reflections.invokeMethod(config, "getInt", weaponTitle + ".Shoot.Delay_Between_Shots", 0).get();
|
||||
if(shootDelay > 0)
|
||||
return shootDelay;
|
||||
}
|
||||
}
|
||||
catch(NoClassDefFoundError | Exception ignored) {}
|
||||
|
||||
return 10;
|
||||
}
|
||||
|
||||
ItemMeta meta = mainHand.getItemMeta();
|
||||
if(meta == null)
|
||||
return 5;
|
||||
|
||||
Collection<AttributeModifier> modifiers = meta.getAttributeModifiers(Attribute.GENERIC_ATTACK_SPEED);
|
||||
if(modifiers == null)
|
||||
return 5;
|
||||
|
||||
double speed = 0;
|
||||
for(AttributeModifier modifier : modifiers)
|
||||
speed += modifier.getAmount();
|
||||
|
||||
return (int) (20 / speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an attack on the target.
|
||||
*/
|
||||
private void performAttack(@NotNull NPC npc)
|
||||
{
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
|
||||
String weaponTitle = getWeaponTitle(mainHand);
|
||||
if(weaponTitle != null)
|
||||
performWeaponMechanicsAttack(npc, weaponTitle, mainHand);
|
||||
else if(mainHand != null && isRangedWeapon(mainHand.getType()))
|
||||
performRangedAttack(npc);
|
||||
else
|
||||
performMeleeAttack(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilizes WeaponMechanics API to gather weapon identification title safely.
|
||||
*/
|
||||
private String getWeaponTitle(ItemStack item)
|
||||
{
|
||||
if(item == null || item.getType() == Material.AIR)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return (String) Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanicsAPI", "getWeaponTitle", item).get();
|
||||
}
|
||||
catch (NoClassDefFoundError | Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes native programmatic WeaponMechanics shooting actions centered around the NPC.
|
||||
*/
|
||||
private void performWeaponMechanicsAttack(@NotNull NPC npc, String weaponTitle, ItemStack weaponStack)
|
||||
{
|
||||
if(target == null || !target.isValid())
|
||||
return;
|
||||
|
||||
Player npcPlayer = npc.getServerPlayer().getBukkitPlayer();
|
||||
if(npcPlayer == null)
|
||||
return;
|
||||
|
||||
if(target == null || !target.isValid())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Vector direction = target.getLocation().subtract(npc.getLocation()).toVector().normalize();
|
||||
Location location = npc.getLocation().clone().add(0, npc.getEyeHeight(null), 0);
|
||||
location.setDirection(direction);
|
||||
|
||||
Object entityWrapper = Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanics", "getInstance")
|
||||
.thanInvoke("getEntityWrapper", npcPlayer).get();
|
||||
Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanics", "getInstance")
|
||||
.thanInvoke("getWeaponHandler").thanInvoke("getShootHandler")
|
||||
.thanInvoke("shoot", entityWrapper, weaponTitle, weaponStack, location, true, true, false);
|
||||
}
|
||||
catch(NoClassDefFoundError | Exception e)
|
||||
{
|
||||
target.damage(getAttackDamage(npc), npcPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a ranged attack.
|
||||
*/
|
||||
private void performRangedAttack(@NotNull NPC npc)
|
||||
{
|
||||
if(target == null || !target.isValid() || isUsing)
|
||||
return;
|
||||
|
||||
double eyeHeight = (npc.entity.getBukkitPlayer() instanceof LivingEntity le ? le.getEyeHeight() : npc.entity.getBukkitPlayer().getHeight()) -
|
||||
(Pose.fromBukkit(npc.getOption(NpcOption.POSE)) == Pose.SITTING ? 0.625 : 0);
|
||||
|
||||
Location npcLoc = npc.getLocation().clone().add(0, eyeHeight * npc.getOption(NpcOption.SCALE), 0);
|
||||
|
||||
final float speed = 2.5f;
|
||||
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
|
||||
if(mainHand != null && (mainHand.getType() == Material.BOW || mainHand.getType() == Material.CROSSBOW))
|
||||
{
|
||||
setUsingItemState(npc, true);
|
||||
isUsing = true;
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(NpcApi.plugin, () ->
|
||||
{
|
||||
setUsingItemState(npc, false);
|
||||
|
||||
isUsing = false;
|
||||
|
||||
if(target == null)
|
||||
return;
|
||||
|
||||
Location targetLoc = target.getLocation().clone().add(0, target.getEyeHeight(), 0);
|
||||
Vector direction = targetLoc.toVector().subtract(npcLoc.toVector()).normalize();
|
||||
|
||||
AbstractArrow arrow;
|
||||
if(mainHand == null || mainHand.getType() == Material.TRIDENT)
|
||||
arrow = npcLoc.getWorld().spawnArrow(npcLoc, direction, speed, 0, Trident.class);
|
||||
else
|
||||
arrow = npcLoc.getWorld().spawnArrow(npcLoc, direction, speed, 0, Arrow.class);
|
||||
arrow.setShooter(npc.getServerPlayer().getBukkitPlayer());
|
||||
arrow.setDamage(getAttackDamage(npc));
|
||||
arrow.setPickupStatus(AbstractArrow.PickupStatus.DISALLOWED);
|
||||
arrow.setShotFromCrossbow(mainHand.getType() == Material.CROSSBOW);
|
||||
|
||||
PacketWrapper removePacket = new RemoveEntityPacket(arrow.getEntityId());
|
||||
for(Player player : npcLoc.getWorld().getPlayers())
|
||||
{
|
||||
if(!npc.getViewers().contains(player.getUniqueId()))
|
||||
WrappedServerPlayer.fromPlayer(player).sendPacket(removePacket);
|
||||
}
|
||||
}, BOW_DRAW_DELAY_TICKS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the NPC's item usage state (e.g., drawing a bow).
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
* @param using true if the NPC is using an item, false otherwise
|
||||
*/
|
||||
private void setUsingItemState(@NotNull NPC npc, boolean using)
|
||||
{
|
||||
WrappedEntityData data = npc.getServerPlayer().getEntityData();
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(8);
|
||||
|
||||
Byte handValue = data.get(accessor);
|
||||
byte handFlag = handValue == null ? 0 : handValue;
|
||||
|
||||
handFlag = using ? (byte) (handFlag | 0x01) : (byte) (handFlag & ~0x01);
|
||||
data.set(accessor, handFlag);
|
||||
|
||||
if(cachedViewers == null || cachedViewers.size() != npc.getViewers().size())
|
||||
updateCachedViewers(npc);
|
||||
|
||||
PacketWrapper packetWrapper = SetEntityDataPacket.create(npc.getServerPlayer().getId(), data);
|
||||
for(Player viewer : cachedViewers)
|
||||
WrappedServerPlayer.fromPlayer(viewer).sendPacket(packetWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a melee attack.
|
||||
*/
|
||||
private void performMeleeAttack(@NotNull NPC npc)
|
||||
{
|
||||
if(target == null || !target.isValid())
|
||||
return;
|
||||
|
||||
if(cachedViewers == null || cachedViewers.size() != npc.getViewers().size())
|
||||
updateCachedViewers(npc);
|
||||
|
||||
for(Player viewer : cachedViewers)
|
||||
npc.playAnimation(viewer, AnimatePacket.Animation.SWING_MAIN_HAND);
|
||||
|
||||
target.damage(getAttackDamage(npc));
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = target.getLocation();
|
||||
|
||||
double knockbackStrength = getAttackKnockback(npc);
|
||||
double knockbackResistance = getKnockbackResistance(target);
|
||||
double effectiveKnockback = knockbackStrength * (1 - knockbackResistance);
|
||||
|
||||
Vector direction = targetLoc.toVector().subtract(npcLoc.toVector());
|
||||
if(direction.lengthSquared() > Vector.getEpsilon())
|
||||
direction = direction.normalize();
|
||||
|
||||
Vector knockback = direction.multiply(effectiveKnockback);
|
||||
knockback.setY(effectiveKnockback * 0.4);
|
||||
target.setVelocity(knockback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a material is a ranged weapon.
|
||||
*/
|
||||
private boolean isRangedWeapon(@NotNull Material material)
|
||||
{
|
||||
return material == Material.BOW || material == Material.CROSSBOW || material == Material.TRIDENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the NPC is currently using a ranged weapon.
|
||||
*/
|
||||
private boolean isUsingRangedWeapon(@NotNull NPC npc)
|
||||
{
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
return (mainHand != null && isRangedWeapon(mainHand.getType())) || getWeaponTitle(mainHand) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attack damage based on the NPC's held item.
|
||||
*/
|
||||
private double getAttackDamage(@NotNull NPC npc)
|
||||
{
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
return getAttackDamage(mainHand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attack damage from an item stack.
|
||||
*
|
||||
* @param item the item to check
|
||||
* @return the attack damage value
|
||||
*/
|
||||
private double getAttackDamage(ItemStack item)
|
||||
{
|
||||
if(item == null || !item.hasItemMeta())
|
||||
return 0.5;
|
||||
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if(meta == null)
|
||||
return 0.5;
|
||||
|
||||
Collection<AttributeModifier> modifiers = meta.getAttributeModifiers(Attribute.GENERIC_ATTACK_DAMAGE);
|
||||
if(modifiers == null)
|
||||
return 0.5;
|
||||
|
||||
double damage = 0;
|
||||
for(AttributeModifier modifier : modifiers)
|
||||
damage += modifier.getAmount();
|
||||
|
||||
return damage == 0 ? 0.5 : damage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attack knockback strength from the NPC's held item.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return the knockback strength
|
||||
*/
|
||||
private double getAttackKnockback(@NotNull NPC npc)
|
||||
{
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
return getAttackKnockback(mainHand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attack knockback strength from an item stack.
|
||||
*
|
||||
* @param item the item to check
|
||||
* @return the knockback strength
|
||||
*/
|
||||
private double getAttackKnockback(ItemStack item)
|
||||
{
|
||||
if(item == null || !item.hasItemMeta())
|
||||
return 0.5;
|
||||
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if(meta == null)
|
||||
return 0.5;
|
||||
|
||||
Collection<AttributeModifier> modifiers = meta.getAttributeModifiers(Attribute.GENERIC_ATTACK_KNOCKBACK);
|
||||
if(modifiers == null)
|
||||
return 0.5;
|
||||
|
||||
double knockback = 0;
|
||||
for(AttributeModifier modifier : modifiers)
|
||||
knockback += modifier.getAmount();
|
||||
|
||||
return knockback == 0 ? 0.5 : knockback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the knockback resistance of a target entity.
|
||||
*
|
||||
* @param target the entity to check
|
||||
* @return the knockback resistance value (0-1)
|
||||
*/
|
||||
private double getKnockbackResistance(@NotNull LivingEntity target)
|
||||
{
|
||||
if(target.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE) == null)
|
||||
return 0;
|
||||
|
||||
return target.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts movement towards the target location.
|
||||
*/
|
||||
private void startMovement(@NotNull NPC npc, @NotNull Location targetLoc)
|
||||
{
|
||||
stopMovement(npc);
|
||||
|
||||
OptionalInt safeY = LocationUtils.findSafeY(targetLoc);
|
||||
if(safeY.isPresent())
|
||||
targetLoc = new Location(targetLoc.getWorld(), targetLoc.getX(), safeY.getAsInt(), targetLoc.getZ());
|
||||
movementGoal = new WalkToLocationGoal.Builder(targetLoc).speed(speed).build();
|
||||
movementGoal.start(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts kiting by moving away from the target to optimal distance.
|
||||
*/
|
||||
private void startKiting(@NotNull NPC npc)
|
||||
{
|
||||
if(target == null || !target.isValid())
|
||||
return;
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = target.getLocation();
|
||||
|
||||
Vector direction = npcLoc.toVector().subtract(targetLoc.toVector()).normalize();
|
||||
|
||||
Vector retreatPos = targetLoc.toVector().add(direction.multiply(OPTIMAL_RANGED_DISTANCE));
|
||||
|
||||
retreatPos.setX(retreatPos.getX() + (Math.random() - 0.5) * 3);
|
||||
retreatPos.setZ(retreatPos.getZ() + (Math.random() - 0.5) * 3);
|
||||
|
||||
Location retreatLoc = new Location(npcLoc.getWorld(), retreatPos.getX(), npcLoc.getY(), retreatPos.getZ());
|
||||
|
||||
OptionalInt safeY = LocationUtils.findSafeY(retreatLoc);
|
||||
if(safeY.isPresent())
|
||||
retreatLoc = new Location(retreatLoc.getWorld(), retreatLoc.getX(), safeY.getAsInt(), retreatLoc.getZ());
|
||||
|
||||
stopMovement(npc);
|
||||
|
||||
|
||||
WalkToLocationGoal newGoal = new WalkToLocationGoal.Builder(retreatLoc).speed(speed + 0.5).maxIterations(5000).allowDiagonal(true)
|
||||
.completionCallback(walkingResult ->
|
||||
{
|
||||
isKiting = false;
|
||||
movementGoal = null;
|
||||
}).withRotation(false).build();
|
||||
|
||||
movementGoal = newGoal;
|
||||
newGoal.start(npc);
|
||||
|
||||
isKiting = true;
|
||||
pathRecalculationCooldown = WalkToLocationGoal.RECALCULATION_COOLDOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops kiting and prepares to attack.
|
||||
*/
|
||||
private void stopKiting(@NotNull NPC npc)
|
||||
{
|
||||
isKiting = false;
|
||||
stopMovement(npc);
|
||||
attackCooldown = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cached viewers list.
|
||||
*/
|
||||
private void updateCachedViewers(@NotNull NPC npc)
|
||||
{
|
||||
cachedViewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops any ongoing movement.
|
||||
*/
|
||||
private void stopMovement(@NotNull NPC npc)
|
||||
{
|
||||
if(movementGoal != null)
|
||||
{
|
||||
movementGoal.stop(npc);
|
||||
movementGoal = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.utils.LocationUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC follow a target entity. The NPC will maintain a specified distance from the target and pathfind to them if too far.
|
||||
*/
|
||||
public class FollowEntityGoal extends Goal
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final double DEFAULT_FOLLOW_DISTANCE = 10.0;
|
||||
public static final double DEFAULT_STOP_DISTANCE = 1.5;
|
||||
|
||||
private double followDistance;
|
||||
private double stopDistance;
|
||||
private double speed;
|
||||
private UUID targetEntityId;
|
||||
|
||||
private transient WalkToLocationGoal currentWalkGoal;
|
||||
private transient LivingEntity target;
|
||||
private transient int pathRecalculationCooldown;
|
||||
private transient Location lastTargetLocation;
|
||||
private transient List<Player> cachedViewers;
|
||||
|
||||
/**
|
||||
* Creates a FollowEntityGoal with a fixed target entity ID and default distances.
|
||||
*
|
||||
* @param targetEntityId The UUID of the entity to follow
|
||||
*/
|
||||
public FollowEntityGoal(@NotNull UUID targetEntityId)
|
||||
{
|
||||
this(targetEntityId, DEFAULT_FOLLOW_DISTANCE, DEFAULT_STOP_DISTANCE, WalkToLocationGoal.DEFAULT_SPEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a FollowEntityGoal with a fixed target entity ID and full configuration.
|
||||
*
|
||||
* @param targetEntityId The UUID of the entity to follow
|
||||
* @param followDistance The distance at which the NPC will start following
|
||||
* @param stopDistance The distance at which the NPC will stop moving
|
||||
* @param speed The walking speed
|
||||
*/
|
||||
public FollowEntityGoal(@NotNull UUID targetEntityId, double followDistance, double stopDistance, double speed)
|
||||
{
|
||||
super(Priority.ALWAYS);
|
||||
this.targetEntityId = targetEntityId;
|
||||
this.followDistance = Math.max(stopDistance + 0.5, followDistance);
|
||||
this.stopDistance = Math.max(0.5, stopDistance);
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private FollowEntityGoal(@NotNull FollowEntityGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.targetEntityId = goal.targetEntityId;
|
||||
this.followDistance = goal.followDistance;
|
||||
this.stopDistance = goal.stopDistance;
|
||||
this.speed = goal.speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the follow distance for this goal.
|
||||
*
|
||||
* @return the follow distance
|
||||
*/
|
||||
public double getFollowDistance()
|
||||
{
|
||||
return followDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the follow distance for this goal.
|
||||
*
|
||||
* @param followDistance the new follow distance
|
||||
*/
|
||||
public void setFollowDistance(double followDistance)
|
||||
{
|
||||
this.followDistance = followDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the stop distance for this goal.
|
||||
*
|
||||
* @return the stop distance
|
||||
*/
|
||||
public double getStopDistance()
|
||||
{
|
||||
return stopDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stop distance for this goal.
|
||||
*
|
||||
* @param stopDistance the new stop distance
|
||||
*/
|
||||
public void setStopDistance(double stopDistance)
|
||||
{
|
||||
this.stopDistance = stopDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed for this goal.
|
||||
*
|
||||
* @return the speed
|
||||
*/
|
||||
public double getSpeed()
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speed for this goal.
|
||||
*
|
||||
* @param speed the new speed
|
||||
*/
|
||||
public void setSpeed(double speed)
|
||||
{
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target entity ID for this goal.
|
||||
*
|
||||
* @return the target entity ID
|
||||
*/
|
||||
public @Nullable UUID getTargetEntityId()
|
||||
{
|
||||
return targetEntityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target entity ID for this goal.
|
||||
*
|
||||
* @param targetEntityId the new target entity ID
|
||||
*/
|
||||
public void setTargetEntityId(@NotNull UUID targetEntityId)
|
||||
{
|
||||
this.targetEntityId = targetEntityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if a valid target exists within follow distance
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
if(targetEntityId == null)
|
||||
return false;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
LivingEntity potentialTarget = target;
|
||||
if(potentialTarget == null || !potentialTarget.isValid())
|
||||
{
|
||||
Entity entity = Bukkit.getEntity(targetEntityId);
|
||||
if(!(entity instanceof LivingEntity le))
|
||||
return false;
|
||||
potentialTarget = le;
|
||||
}
|
||||
|
||||
if(!potentialTarget.isValid())
|
||||
return false;
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = potentialTarget.getLocation();
|
||||
|
||||
if(!npcLoc.getWorld().equals(targetLoc.getWorld()))
|
||||
return false;
|
||||
|
||||
double distance = npcLoc.distance(targetLoc);
|
||||
return distance <= followDistance && distance > stopDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the follow goal, initializing the target and path.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
Entity entity = Bukkit.getEntity(targetEntityId);
|
||||
|
||||
if(!(entity instanceof LivingEntity le))
|
||||
return;
|
||||
|
||||
this.target = le;
|
||||
this.pathRecalculationCooldown = 0;
|
||||
this.lastTargetLocation = target.getLocation().clone();
|
||||
if(target != null && target.isValid())
|
||||
{
|
||||
currentWalkGoal = new WalkToLocationGoal.Builder(target.getLocation()).speed(speed).withRotation(false).build();
|
||||
currentWalkGoal.start(npc);
|
||||
}
|
||||
|
||||
updateCachedViewers(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the follow goal, updating movement and path recalculation.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
if(target == null || !target.isValid())
|
||||
{
|
||||
Entity entity = Bukkit.getEntity(targetEntityId);
|
||||
if(entity instanceof LivingEntity newTarget && newTarget != null && newTarget.isValid())
|
||||
target = newTarget;
|
||||
else
|
||||
{
|
||||
stop(npc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Location targetLoc = target.getLocation().clone();
|
||||
if(cachedViewers == null || cachedViewers.size() != npc.getViewers().size())
|
||||
updateCachedViewers(npc);
|
||||
|
||||
for(Player viewer : cachedViewers)
|
||||
npc.lookAtEntity(target, viewer, true);
|
||||
|
||||
targetLoc.setYaw(npc.getLocation().getYaw());
|
||||
targetLoc.setPitch(npc.getLocation().getPitch());
|
||||
|
||||
OptionalInt safeY = LocationUtils.findSafeY(targetLoc);
|
||||
targetLoc.setY(safeY.isPresent() ? safeY.getAsInt() : targetLoc.getY());
|
||||
|
||||
double distance = npc.getLocation().distance(targetLoc);
|
||||
if(distance <= stopDistance)
|
||||
{
|
||||
if(currentWalkGoal != null)
|
||||
{
|
||||
currentWalkGoal.stop(npc);
|
||||
currentWalkGoal = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentWalkGoal == null)
|
||||
{
|
||||
currentWalkGoal = new WalkToLocationGoal.Builder(targetLoc).speed(speed).withRotation(false).build();
|
||||
currentWalkGoal.start(npc);
|
||||
pathRecalculationCooldown = 10;
|
||||
lastTargetLocation = targetLoc.clone();
|
||||
}
|
||||
else
|
||||
{
|
||||
Location currentTarget = currentWalkGoal.getTargetLocation();
|
||||
boolean shouldRecalculate = currentTarget.distance(targetLoc) > 2.0;
|
||||
|
||||
if(!shouldRecalculate && lastTargetLocation != null)
|
||||
{
|
||||
double verticalChange = Math.abs(targetLoc.getY() - lastTargetLocation.getY());
|
||||
if(verticalChange > 0.5)
|
||||
shouldRecalculate = true;
|
||||
}
|
||||
|
||||
if(!shouldRecalculate && pathRecalculationCooldown <= 0)
|
||||
{
|
||||
shouldRecalculate = true;
|
||||
pathRecalculationCooldown = 10;
|
||||
}
|
||||
|
||||
if(shouldRecalculate)
|
||||
{
|
||||
currentWalkGoal.stop(npc);
|
||||
currentWalkGoal = new WalkToLocationGoal.Builder(targetLoc).speed(speed).withRotation(false).build();
|
||||
currentWalkGoal.start(npc);
|
||||
pathRecalculationCooldown = 10;
|
||||
lastTargetLocation = targetLoc.clone();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentWalkGoal.tick(npc);
|
||||
pathRecalculationCooldown--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the follow goal and cleans up state.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
if(currentWalkGoal != null)
|
||||
{
|
||||
currentWalkGoal.stop(npc);
|
||||
currentWalkGoal = null;
|
||||
}
|
||||
|
||||
if(cachedViewers == null || cachedViewers.size() != npc.getViewers().size())
|
||||
updateCachedViewers(npc);
|
||||
|
||||
for(Player viewer : cachedViewers)
|
||||
npc.lookAtEntity(target, viewer, true);
|
||||
|
||||
target = null;
|
||||
lastTargetLocation = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the target is still valid and within range
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
if(target == null || !target.isValid())
|
||||
return false;
|
||||
|
||||
if(!super.canContinue(npc))
|
||||
return false;
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = target.getLocation();
|
||||
|
||||
if(!npcLoc.getWorld().equals(targetLoc.getWorld()))
|
||||
return false;
|
||||
|
||||
double distance = npcLoc.distance(targetLoc);
|
||||
return distance <= followDistance * 2 && distance > stopDistance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canBeRemovedNow(@NotNull NPC npc)
|
||||
{
|
||||
if(super.canBeRemovedNow(npc))
|
||||
return true;
|
||||
|
||||
if(currentWalkGoal != null && currentWalkGoal.canContinue(npc))
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location below = npcLoc.clone().subtract(0, 0.1, 0);
|
||||
|
||||
if(below.getBlock().getType().isAir())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new FollowEntityGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cached viewers list.
|
||||
*/
|
||||
private void updateCachedViewers(@NotNull NPC npc)
|
||||
{
|
||||
cachedViewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.wrapper.enums.Pose;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC look around randomly. This can serve as a "wait" goal or an idle behavior when no other goals are active.
|
||||
*/
|
||||
public class LookAroundGoal extends Goal
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int DEFAULT_MIN_DURATION = 20; // 1 second
|
||||
public static final int DEFAULT_MAX_DURATION = 80; // 4 seconds
|
||||
private static final float YAW_CHANGE_MAX = 120f;
|
||||
private static final float PITCH_CHANGE_MAX = 40f;
|
||||
private static final double ENTITY_LOOK_RANGE = 8.0;
|
||||
private static final double ENTITY_LOOK_CHANCE = 0.25;
|
||||
private static final double STARING_CHANCE = 0.2;
|
||||
private static final float PITCH_MIN_MAX = 50f;
|
||||
private static final int MIN_STARE_DURATION = 60; // 2 seconds
|
||||
private static final int MAX_STARE_DURATION = 120; // 5 seconds
|
||||
|
||||
private int minDuration;
|
||||
private int maxDuration;
|
||||
|
||||
private transient int ticksRemaining;
|
||||
private transient float targetYaw;
|
||||
private transient float targetPitch;
|
||||
private transient boolean isLooking;
|
||||
private transient float rotationSpeed;
|
||||
private transient LivingEntity staringEntity;
|
||||
|
||||
/**
|
||||
* Creates a LookAroundGoal with default duration.
|
||||
*/
|
||||
public LookAroundGoal()
|
||||
{
|
||||
this(DEFAULT_MIN_DURATION, DEFAULT_MAX_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LookAroundGoal with custom duration range.
|
||||
*
|
||||
* @param minDuration Minimum duration in ticks
|
||||
* @param maxDuration Maximum duration in ticks
|
||||
*/
|
||||
public LookAroundGoal(int minDuration, int maxDuration)
|
||||
{
|
||||
super(Priority.LOW);
|
||||
this.minDuration = minDuration;
|
||||
this.maxDuration = maxDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private LookAroundGoal(@NotNull LookAroundGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.minDuration = goal.minDuration;
|
||||
this.maxDuration = goal.maxDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum duration for this goal.
|
||||
*
|
||||
* @return the maximum duration
|
||||
*/
|
||||
public int getMaxDuration()
|
||||
{
|
||||
return maxDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum duration for this goal.
|
||||
*
|
||||
* @param maxDuration the new maximum duration
|
||||
*/
|
||||
public void setMaxDuration(int maxDuration)
|
||||
{
|
||||
this.maxDuration = maxDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum duration for this goal.
|
||||
*
|
||||
* @return the minimum duration
|
||||
*/
|
||||
public int getMinDuration()
|
||||
{
|
||||
return minDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum duration for this goal.
|
||||
*
|
||||
* @param minDuration the new minimum duration
|
||||
*/
|
||||
public void setMinDuration(int minDuration)
|
||||
{
|
||||
this.minDuration = minDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true always (this goal can always be used)
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return minDuration >= 0 && maxDuration >= 0 && super.canUse(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the look around goal, initializing rotation state.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
ticksRemaining = minDuration + ThreadLocalRandom.current().nextInt(maxDuration - minDuration);
|
||||
isLooking = true;
|
||||
rotationSpeed = 0.05f + ThreadLocalRandom.current().nextFloat() * 0.15f;
|
||||
pickNewLookDirection(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the look around goal, updating head rotation.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
if(ticksRemaining <= 0 && staringEntity == null)
|
||||
{
|
||||
decideNextAction(npc);
|
||||
ticksRemaining = minDuration + ThreadLocalRandom.current().nextInt(maxDuration - minDuration);
|
||||
}
|
||||
|
||||
if(staringEntity != null && ticksRemaining > 0)
|
||||
lookAtEntity(npc, staringEntity);
|
||||
else if(staringEntity != null)
|
||||
staringEntity = null;
|
||||
|
||||
Location currentLoc = npc.getLocation();
|
||||
float currentYaw = currentLoc.getYaw();
|
||||
float currentPitch = currentLoc.getPitch();
|
||||
|
||||
float newYaw, newPitch;
|
||||
if(staringEntity != null && ticksRemaining > 0)
|
||||
{
|
||||
newYaw = targetYaw;
|
||||
newPitch = targetPitch;
|
||||
}
|
||||
else
|
||||
{
|
||||
newYaw = lerpAngle(currentYaw, targetYaw, rotationSpeed);
|
||||
newPitch = lerp(currentPitch, targetPitch, rotationSpeed);
|
||||
}
|
||||
|
||||
newPitch = Math.max(-PITCH_MIN_MAX, Math.min(PITCH_MIN_MAX, newPitch));
|
||||
|
||||
currentLoc.setYaw(newYaw);
|
||||
currentLoc.setPitch(newPitch);
|
||||
|
||||
npc.rotateHead(newYaw, newPitch);
|
||||
npc.setLocation(currentLoc);
|
||||
|
||||
ticksRemaining--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the look around goal and cleans up state.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
isLooking = false;
|
||||
ticksRemaining = 0;
|
||||
staringEntity = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the NPC is still looking
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return isLooking && super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new LookAroundGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides the next action: look around randomly, stare at current direction, or look at a nearby entity.
|
||||
*/
|
||||
private void decideNextAction(@NotNull NPC npc)
|
||||
{
|
||||
if(ThreadLocalRandom.current().nextDouble() < ENTITY_LOOK_CHANCE)
|
||||
{
|
||||
LivingEntity nearbyEntity = findNearbyEntity(npc);
|
||||
if(nearbyEntity != null)
|
||||
{
|
||||
staringEntity = nearbyEntity;
|
||||
ticksRemaining = MIN_STARE_DURATION + ThreadLocalRandom.current().nextInt(MAX_STARE_DURATION - MIN_STARE_DURATION);
|
||||
lookAtEntity(npc, nearbyEntity);
|
||||
rotationSpeed = 0.1f + ThreadLocalRandom.current().nextFloat() * 0.1f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(ThreadLocalRandom.current().nextDouble() < STARING_CHANCE)
|
||||
rotationSpeed = 0.02f + ThreadLocalRandom.current().nextFloat() * 0.03f;
|
||||
else
|
||||
{
|
||||
pickNewLookDirection(npc);
|
||||
rotationSpeed = 0.05f + ThreadLocalRandom.current().nextFloat() * 0.15f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a nearby living entity to look at.
|
||||
*/
|
||||
private LivingEntity findNearbyEntity(@NotNull NPC npc)
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
for(Entity entity : npcLoc.getWorld().getNearbyEntities(npcLoc, ENTITY_LOOK_RANGE, ENTITY_LOOK_RANGE, ENTITY_LOOK_RANGE))
|
||||
{
|
||||
if(entity instanceof LivingEntity livingEntity && !entity.equals(npc.entity.getBukkitPlayer()))
|
||||
return livingEntity;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target direction to look at an entity.
|
||||
*/
|
||||
private void lookAtEntity(@NotNull NPC npc, @NotNull LivingEntity entity)
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = entity.getLocation();
|
||||
|
||||
double dx = targetLoc.getX() - npcLoc.getX();
|
||||
|
||||
double eyeHeight = (npc.entity.getBukkitPlayer() instanceof LivingEntity le ? le.getEyeHeight() :
|
||||
npc.entity.getBukkitPlayer().getHeight()) - (Pose.fromBukkit(npc.getOption(NpcOption.POSE)) == Pose.SITTING ? 0.625 : 0);
|
||||
|
||||
double dy = (targetLoc.getY() + entity.getEyeHeight()) - (npcLoc.getY() + (eyeHeight * npc.getOption(NpcOption.SCALE)));
|
||||
double dz = targetLoc.getZ() - npcLoc.getZ();
|
||||
|
||||
double distanceXZ = Math.sqrt(dx * dx + dz * dz);
|
||||
targetYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
targetPitch = (float) Math.toDegrees(-Math.atan2(dy, distanceXZ));
|
||||
|
||||
targetPitch = Math.max(-PITCH_MIN_MAX, Math.min(PITCH_MIN_MAX, targetPitch));
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks a new random look direction.
|
||||
*/
|
||||
private void pickNewLookDirection(@NotNull NPC npc)
|
||||
{
|
||||
Location currentLoc = npc.getLocation();
|
||||
float currentYaw = currentLoc.getYaw();
|
||||
float currentPitch = currentLoc.getPitch();
|
||||
|
||||
float yawMultiplier = ThreadLocalRandom.current().nextDouble() < 0.3 ? 1.5f : 1.0f;
|
||||
targetYaw = currentYaw + (ThreadLocalRandom.current().nextFloat() * 2 - 1) * YAW_CHANGE_MAX * yawMultiplier;
|
||||
|
||||
float pitchUpAvailable = PITCH_MIN_MAX - currentPitch;
|
||||
float pitchDownAvailable = currentPitch - (-PITCH_MIN_MAX);
|
||||
float maxPitchChange = Math.min(PITCH_CHANGE_MAX, Math.min(pitchUpAvailable, pitchDownAvailable) * 2f);
|
||||
|
||||
if(pitchUpAvailable < 5f)
|
||||
targetPitch = currentPitch - ThreadLocalRandom.current().nextFloat() * PITCH_CHANGE_MAX;
|
||||
else if(pitchDownAvailable < 5f)
|
||||
targetPitch = currentPitch + ThreadLocalRandom.current().nextFloat() * PITCH_CHANGE_MAX;
|
||||
else
|
||||
targetPitch = currentPitch + (ThreadLocalRandom.current().nextFloat() * 2 - 1) * maxPitchChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linearly interpolates between two angles, handling wraparound.
|
||||
*/
|
||||
private float lerpAngle(float from, float to, float alpha)
|
||||
{
|
||||
float diff = to - from;
|
||||
while(diff < -180)
|
||||
diff += 360;
|
||||
while(diff > 180)
|
||||
diff -= 360;
|
||||
return from + diff * alpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linearly interpolates between two values.
|
||||
*/
|
||||
private float lerp(float from, float to, float alpha)
|
||||
{
|
||||
return from + (to - from) * alpha;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC wait/idle for a specified duration. This is useful for creating pauses between actions.
|
||||
*/
|
||||
public class WaitGoal extends Goal
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int durationTicks;
|
||||
private transient int ticksRemaining;
|
||||
|
||||
/**
|
||||
* Creates a WaitGoal with the specified duration.
|
||||
*
|
||||
* @param durationTicks The duration to wait in ticks (20 ticks = 1 second)
|
||||
*/
|
||||
public WaitGoal(int durationTicks)
|
||||
{
|
||||
super(Priority.LOW);
|
||||
this.durationTicks = Math.max(1, durationTicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private WaitGoal(@NotNull WaitGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.durationTicks = goal.durationTicks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration in ticks.
|
||||
*
|
||||
* @return The duration in ticks
|
||||
*/
|
||||
public int getDurationTicks()
|
||||
{
|
||||
return durationTicks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration for this goal.
|
||||
*
|
||||
* @param durationTicks the new duration in ticks
|
||||
*/
|
||||
public void setDurationTicks(int durationTicks)
|
||||
{
|
||||
this.durationTicks = Math.max(1, durationTicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if there are remaining ticks or duration is set
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return (ticksRemaining > 0 || durationTicks > 0) && super.canUse(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the wait goal, initializing the tick counter.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
ticksRemaining = durationTicks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the wait goal, decrementing the remaining ticks.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
ticksRemaining--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the wait goal and resets the tick counter.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
ticksRemaining = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if there are remaining ticks
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return ticksRemaining > 0 && super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new WaitGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the remaining ticks.
|
||||
*
|
||||
* @return The remaining ticks
|
||||
*/
|
||||
public int getTicksRemaining()
|
||||
{
|
||||
return ticksRemaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the wait duration.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
ticksRemaining = durationTicks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.enums.WalkingResult;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import de.eisi05.npc.api.utils.SerializableConsumer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC walk along a predefined path of waypoints.
|
||||
* The NPC will walk the entire path without recalculating, using the pre-calculated path.
|
||||
*/
|
||||
public class WalkPathGoal extends Goal
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final double DEFAULT_SPEED = 0.25;
|
||||
private static final double START_LOCATION_THRESHOLD = 50.0 * 50.0;
|
||||
|
||||
private Path.SerializablePath serializablePath;
|
||||
private double speed;
|
||||
private SerializableConsumer<WalkingResult> completionCallback;
|
||||
private boolean withRotation;
|
||||
|
||||
private transient Path path;
|
||||
private transient boolean isWalking;
|
||||
private transient WalkToLocationGoal walkToStartGoal;
|
||||
private transient boolean walkingToStart;
|
||||
|
||||
/**
|
||||
* Creates a WalkPathGoal with full configuration options.
|
||||
*
|
||||
* @param builder the builder containing the configuration options
|
||||
*/
|
||||
public WalkPathGoal(@NotNull Builder builder)
|
||||
{
|
||||
super(Priority.MEDIUM);
|
||||
this.path = builder.path;
|
||||
this.serializablePath = path != null ? path.toSerializablePath() : null;
|
||||
this.speed = Math.max(0.1, Math.min(1.0, builder.speed));
|
||||
this.completionCallback = builder.completionCallback;
|
||||
this.withRotation = builder.withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private WalkPathGoal(@NotNull WalkPathGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.serializablePath = goal.serializablePath;
|
||||
this.speed = goal.speed;
|
||||
this.completionCallback = goal.completionCallback;
|
||||
this.withRotation = goal.withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path for this goal.
|
||||
*
|
||||
* @return the path
|
||||
*/
|
||||
public @Nullable Path getPath()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the path for this goal.
|
||||
*
|
||||
* @param path the new path
|
||||
*/
|
||||
public void setPath(@NotNull Path path)
|
||||
{
|
||||
this.path = path;
|
||||
this.serializablePath = path.toSerializablePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed for this goal.
|
||||
*
|
||||
* @return the speed
|
||||
*/
|
||||
public double getSpeed()
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speed for this goal.
|
||||
*
|
||||
* @param speed the new speed
|
||||
*/
|
||||
public void setSpeed(double speed)
|
||||
{
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the completion callback for the walk path goal.
|
||||
*
|
||||
* @return the completion callback
|
||||
*/
|
||||
public @Nullable SerializableConsumer<WalkingResult> getCompletionCallback()
|
||||
{
|
||||
return completionCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completion callback for the walk path goal.
|
||||
*
|
||||
* @param completionCallback the new completion callback
|
||||
*/
|
||||
public void setCompletionCallback(@Nullable SerializableConsumer<WalkingResult> completionCallback)
|
||||
{
|
||||
this.completionCallback = completionCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the NPC should rotate to face the target location.
|
||||
*
|
||||
* @return true if the NPC should rotate
|
||||
*/
|
||||
public boolean isWithRotation()
|
||||
{
|
||||
return withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the NPC should rotate to face the target location.
|
||||
*
|
||||
* @param withRotation the new rotation setting
|
||||
*/
|
||||
public void setWithRotation(boolean withRotation)
|
||||
{
|
||||
this.withRotation = withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the path is valid, start location is close enough, and not already walking
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
if(isWalking)
|
||||
return true;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
if(path == null)
|
||||
return false;
|
||||
|
||||
List<Location> waypoints = path.asLocations();
|
||||
if(waypoints == null || waypoints.isEmpty())
|
||||
return false;
|
||||
|
||||
Location startLocation = waypoints.get(0);
|
||||
if(!startLocation.getWorld().equals(npc.getLocation().getWorld()))
|
||||
return false;
|
||||
|
||||
double distance = npc.getLocation().distanceSquared(startLocation);
|
||||
return distance <= START_LOCATION_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the walk path goal, first walking to the start location if needed, then walking the entire path.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
if(path == null)
|
||||
return;
|
||||
|
||||
List<Location> waypoints = path.asLocations();
|
||||
if(waypoints == null || waypoints.isEmpty())
|
||||
return;
|
||||
|
||||
Location startLocation = waypoints.get(0);
|
||||
double distance = npc.getLocation().distance(startLocation);
|
||||
|
||||
isWalking = true;
|
||||
|
||||
if(distance > START_LOCATION_THRESHOLD)
|
||||
{
|
||||
walkingToStart = true;
|
||||
walkToStartGoal = new WalkToLocationGoal.Builder(startLocation)
|
||||
.speed(speed)
|
||||
.withRotation(withRotation)
|
||||
.completionCallback(result ->
|
||||
{
|
||||
walkToStartGoal = null;
|
||||
walkingToStart = false;
|
||||
|
||||
if(result == WalkingResult.SUCCESS)
|
||||
walkPath(npc);
|
||||
else
|
||||
{
|
||||
isWalking = false;
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(result);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
walkToStartGoal.start(npc);
|
||||
}
|
||||
else
|
||||
walkPath(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the actual path after reaching the start location.
|
||||
*
|
||||
* @param npc the NPC to walk
|
||||
*/
|
||||
private void walkPath(@NotNull NPC npc)
|
||||
{
|
||||
if(path == null)
|
||||
return;
|
||||
|
||||
List<Location> waypoints = path.asLocations();
|
||||
if(waypoints == null || waypoints.isEmpty())
|
||||
return;
|
||||
|
||||
List<Player> viewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
Location finalLocation = waypoints.get(waypoints.size() - 1);
|
||||
|
||||
npc.walkTo(path, speed, true, result ->
|
||||
{
|
||||
isWalking = false;
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(result);
|
||||
if(result == WalkingResult.SUCCESS)
|
||||
npc.changeRealLocation(finalLocation);
|
||||
}, withRotation, viewers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the walk path goal, handling both walking to start and walking the path.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
if(walkingToStart && walkToStartGoal != null)
|
||||
{
|
||||
walkToStartGoal.tick(npc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the walk path goal and cancels current walking.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
if(!isWalking)
|
||||
return;
|
||||
|
||||
if(walkToStartGoal != null)
|
||||
{
|
||||
walkToStartGoal.stop(npc);
|
||||
walkToStartGoal = null;
|
||||
}
|
||||
|
||||
List<Player> viewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
for(Player viewer : viewers)
|
||||
{
|
||||
if(npc.isWalking(viewer))
|
||||
npc.cancelWalking(viewer);
|
||||
}
|
||||
|
||||
isWalking = false;
|
||||
walkingToStart = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if still walking and has waypoints remaining
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return isWalking && super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canBeRemovedNow(@NotNull NPC npc)
|
||||
{
|
||||
if(super.canBeRemovedNow(npc))
|
||||
return true;
|
||||
|
||||
if(isWalking)
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location below = npcLoc.clone().subtract(0, 0.1, 0);
|
||||
|
||||
if(below.getBlock().getType().isAir())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new WalkPathGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for creating WalkPathGoal instances with a fluent API.
|
||||
*/
|
||||
public static class Builder
|
||||
{
|
||||
private final Path path;
|
||||
private double speed = DEFAULT_SPEED;
|
||||
private SerializableConsumer<WalkingResult> completionCallback;
|
||||
private boolean withRotation = true;
|
||||
|
||||
/**
|
||||
* Creates a new Builder with the required path.
|
||||
*
|
||||
* @param path The path to walk along
|
||||
*/
|
||||
public Builder(@NotNull Path path)
|
||||
{
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the walking speed.
|
||||
*
|
||||
* @param speed The walking speed (0.1 to 1.0)
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder speed(double speed)
|
||||
{
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completion callback.
|
||||
*
|
||||
* @param completionCallback Callback called when path walking completes
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder completionCallback(@NotNull SerializableConsumer<WalkingResult> completionCallback)
|
||||
{
|
||||
this.completionCallback = completionCallback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether rotation packets should be sent.
|
||||
*
|
||||
* @param withRotation If true, includes rotation packets in the movement; otherwise only position packets are sent.
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder withRotation(boolean withRotation)
|
||||
{
|
||||
this.withRotation = withRotation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the WalkPathGoal instance.
|
||||
*
|
||||
* @return A new WalkPathGoal instance
|
||||
*/
|
||||
public WalkPathGoal build()
|
||||
{
|
||||
return new WalkPathGoal(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.enums.WalkingResult;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.pathfinding.AStarPathfinder;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import de.eisi05.npc.api.pathfinding.PathfindingUtils;
|
||||
import de.eisi05.npc.api.scheduler.Tasks;
|
||||
import de.eisi05.npc.api.utils.SerializableConsumer;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC walk to a specific location using pathfinding.
|
||||
*/
|
||||
public class WalkToLocationGoal extends Goal
|
||||
{
|
||||
public static final int DEFAULT_MAX_ITERATIONS = 5000;
|
||||
public static final double DEFAULT_SPEED = 0.25;
|
||||
static final int RECALCULATION_COOLDOWN = 20;
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final int PATH_CHECK_AHEAD = 5;
|
||||
private static final long PATHABILITY_CHECK_INTERVAL_MS = 5000;
|
||||
|
||||
private Path.SerializablePath.SerializableLocation serializableLocation;
|
||||
private double speed;
|
||||
private int maxIterations;
|
||||
private boolean allowDiagonal;
|
||||
private SerializableConsumer<WalkingResult> completionCallback;
|
||||
private boolean withRotation;
|
||||
|
||||
private transient Location targetLocation;
|
||||
private transient CompletableFuture<Path> pathfindingFuture;
|
||||
private transient Path currentPath;
|
||||
private transient boolean isWalking;
|
||||
private transient int pathRecalculationCooldown = 0;
|
||||
private transient boolean pathable = true;
|
||||
private transient long lastPathabilityCheckTime = 0;
|
||||
|
||||
/**
|
||||
* Creates a WalkToLocationGoal with full configuration options.
|
||||
*
|
||||
* @param builder the builder containing the configuration options
|
||||
*/
|
||||
public WalkToLocationGoal(@NotNull Builder builder)
|
||||
{
|
||||
super(Priority.MEDIUM);
|
||||
this.targetLocation = builder.targetLocation.clone();
|
||||
this.serializableLocation = new Path.SerializablePath.SerializableLocation(targetLocation);
|
||||
this.speed = Math.max(0.1, Math.min(1.0, builder.speed));
|
||||
this.maxIterations = builder.maxIterations;
|
||||
this.allowDiagonal = builder.allowDiagonal;
|
||||
this.completionCallback = builder.completionCallback;
|
||||
this.withRotation = builder.withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private WalkToLocationGoal(@NotNull WalkToLocationGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.serializableLocation = goal.serializableLocation;
|
||||
this.speed = goal.speed;
|
||||
this.maxIterations = goal.maxIterations;
|
||||
this.allowDiagonal = goal.allowDiagonal;
|
||||
this.completionCallback = goal.completionCallback;
|
||||
this.withRotation = goal.withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed for this goal.
|
||||
*
|
||||
* @return the speed
|
||||
*/
|
||||
public double getSpeed()
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speed for this goal.
|
||||
*
|
||||
* @param speed the new speed
|
||||
*/
|
||||
public void setSpeed(double speed)
|
||||
{
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum number of iterations for pathfinding.
|
||||
*
|
||||
* @return the maximum number of iterations
|
||||
*/
|
||||
public int getMaxIterations()
|
||||
{
|
||||
return maxIterations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of iterations for pathfinding.
|
||||
*
|
||||
* @param maxIterations the new maximum number of iterations
|
||||
*/
|
||||
public void setMaxIterations(int maxIterations)
|
||||
{
|
||||
this.maxIterations = maxIterations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether diagonal movement is allowed.
|
||||
*
|
||||
* @return true if diagonal movement is allowed
|
||||
*/
|
||||
public boolean isAllowDiagonal()
|
||||
{
|
||||
return allowDiagonal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether diagonal movement is allowed.
|
||||
*
|
||||
* @param allowDiagonal the new diagonal movement setting
|
||||
*/
|
||||
public void setAllowDiagonal(boolean allowDiagonal)
|
||||
{
|
||||
this.allowDiagonal = allowDiagonal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the completion callback for the walk to location goal.
|
||||
*
|
||||
* @return the completion callback
|
||||
*/
|
||||
public @Nullable SerializableConsumer<WalkingResult> getCompletionCallback()
|
||||
{
|
||||
return completionCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completion callback for the walk to location goal.
|
||||
*
|
||||
* @param completionCallback the new completion callback
|
||||
*/
|
||||
public void setCompletionCallback(@Nullable SerializableConsumer<WalkingResult> completionCallback)
|
||||
{
|
||||
this.completionCallback = completionCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the NPC should rotate to face the target location.
|
||||
*
|
||||
* @return true if the NPC should rotate
|
||||
*/
|
||||
public boolean isWithRotation()
|
||||
{
|
||||
return withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the NPC should rotate to face the target location.
|
||||
*
|
||||
* @param withRotation the new rotation setting
|
||||
*/
|
||||
public void setWithRotation(boolean withRotation)
|
||||
{
|
||||
this.withRotation = withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the NPC is not already walking and target is valid
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
if(isWalking)
|
||||
return true;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
if(targetLocation == null || !targetLocation.getWorld().equals(npc.getLocation().getWorld()))
|
||||
return false;
|
||||
|
||||
if(npc.getLocation().distance(targetLocation) <= 1.0)
|
||||
return false;
|
||||
|
||||
if(lastPathabilityCheckTime == 0 || System.currentTimeMillis() - lastPathabilityCheckTime > PATHABILITY_CHECK_INTERVAL_MS)
|
||||
checkPathabilityAsync(npc);
|
||||
|
||||
if(!pathable)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the walk to location goal, calculating the path.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
if(targetLocation == null)
|
||||
return;
|
||||
|
||||
calculatePath(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the walk to location goal, checking for path validity and recalculating if needed.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
if(pathRecalculationCooldown > 0)
|
||||
{
|
||||
pathRecalculationCooldown--;
|
||||
return;
|
||||
}
|
||||
|
||||
if(isPathInvalid(npc))
|
||||
{
|
||||
calculatePath(npc);
|
||||
pathRecalculationCooldown = RECALCULATION_COOLDOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the walk to location goal and cancels pathfinding.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
if(pathfindingFuture != null)
|
||||
{
|
||||
pathfindingFuture.cancel(true);
|
||||
pathfindingFuture = null;
|
||||
}
|
||||
|
||||
cancelWalking(npc);
|
||||
currentPath = null;
|
||||
pathable = true;
|
||||
lastPathabilityCheckTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the NPC is still walking and not at target
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return isWalking && currentPath != null && targetLocation != null && npc.getLocation().distance(targetLocation) > 1.0
|
||||
&& super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canBeRemovedNow(@NotNull NPC npc)
|
||||
{
|
||||
if(super.canBeRemovedNow(npc))
|
||||
return true;
|
||||
|
||||
if(isWalking)
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location below = npcLoc.clone().subtract(0, 0.1, 0);
|
||||
|
||||
if(below.getBlock().getType().isAir())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new WalkToLocationGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target location.
|
||||
*
|
||||
* @return The target location
|
||||
*/
|
||||
public @NotNull Location getTargetLocation()
|
||||
{
|
||||
return targetLocation.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target location for this goal.
|
||||
*
|
||||
* @param targetLocation the new target location
|
||||
*/
|
||||
public void setTargetLocation(@NotNull Location targetLocation)
|
||||
{
|
||||
this.targetLocation = targetLocation;
|
||||
this.serializableLocation = new Path.SerializablePath.SerializableLocation(targetLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously checks if a path exists to the target location. Updates the pathable cache based on the result.
|
||||
*
|
||||
* @param npc the NPC to check pathability for
|
||||
*/
|
||||
private void checkPathabilityAsync(@NotNull NPC npc)
|
||||
{
|
||||
lastPathabilityCheckTime = System.currentTimeMillis();
|
||||
Location start = npc.getLocation();
|
||||
Location end = targetLocation;
|
||||
|
||||
if(end == null || !end.getWorld().equals(start.getWorld()))
|
||||
{
|
||||
pathable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture<Path> future = npc.findPathAsync(List.of(start, end), maxIterations, allowDiagonal, null);
|
||||
Tasks.trackFuture(future);
|
||||
future.thenAcceptAsync(path -> pathable = path != null, task -> Bukkit.getScheduler().runTask(NpcApi.plugin, task))
|
||||
.exceptionally(e ->
|
||||
{
|
||||
pathable = false;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the path to the target location asynchronously.
|
||||
*
|
||||
* @param npc the NPC to calculate path for
|
||||
*/
|
||||
private void calculatePath(@NotNull NPC npc)
|
||||
{
|
||||
Location start = npc.getLocation();
|
||||
Location end = targetLocation;
|
||||
|
||||
if(!end.getWorld().equals(start.getWorld()))
|
||||
{
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(WalkingResult.CANCELLED);
|
||||
cancelWalking(npc);
|
||||
return;
|
||||
}
|
||||
|
||||
isWalking = true;
|
||||
|
||||
pathfindingFuture = npc.findPathAsync(List.of(start, end), maxIterations, allowDiagonal, null);
|
||||
Tasks.trackFuture(pathfindingFuture);
|
||||
pathfindingFuture.thenAcceptAsync(path ->
|
||||
{
|
||||
isWalking = false;
|
||||
if(path != null)
|
||||
{
|
||||
currentPath = path;
|
||||
startWalking(npc);
|
||||
}
|
||||
else
|
||||
{
|
||||
cancelWalking(npc);
|
||||
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(WalkingResult.CANCELLED);
|
||||
}
|
||||
}, task -> Bukkit.getScheduler().runTask(NpcApi.plugin, task))
|
||||
.exceptionally(e ->
|
||||
{
|
||||
Bukkit.getScheduler().runTask(NpcApi.plugin, () ->
|
||||
{
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(WalkingResult.CANCELLED);
|
||||
cancelWalking(npc);
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the actual walking animation along the calculated path.
|
||||
*
|
||||
* @param npc the NPC to start walking
|
||||
*/
|
||||
private void startWalking(@NotNull NPC npc)
|
||||
{
|
||||
List<Player> viewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
npc.walkTo(currentPath, speed, true, result ->
|
||||
{
|
||||
isWalking = false;
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(result);
|
||||
if(result == WalkingResult.SUCCESS)
|
||||
npc.changeRealLocation(targetLocation);
|
||||
}, withRotation, viewers);
|
||||
|
||||
isWalking = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current path is invalid due to block changes. Uses similar validation logic to AStarPathfinder for consistency.
|
||||
*
|
||||
* @param npc the NPC to check path for
|
||||
* @return true if the path is invalid and should be recalculated
|
||||
*/
|
||||
private boolean isPathInvalid(@NotNull NPC npc)
|
||||
{
|
||||
if(currentPath == null || currentPath.getWaypoints().isEmpty())
|
||||
return true;
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
if(npcLoc.getWorld() == null)
|
||||
return true;
|
||||
|
||||
WrappedEntity.BoundingBox boundingBox = npc.entity.getBoundingBox();
|
||||
double scale = npc.getOption(NpcOption.SCALE);
|
||||
int checkAhead = Math.min(PATH_CHECK_AHEAD, currentPath.getWaypoints().size());
|
||||
for(int i = 0; i < checkAhead; i++)
|
||||
{
|
||||
Location waypoint = currentPath.getWaypoints().get(i);
|
||||
if(!waypoint.getWorld().equals(npcLoc.getWorld()))
|
||||
return true;
|
||||
|
||||
if(!AStarPathfinder.isPositionValid(npcLoc.getWorld(), waypoint.getX(), waypoint.getY(), waypoint.getZ(), boundingBox.getYSize() * scale,
|
||||
boundingBox.getXSize() * scale))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the NPC's walking task for all viewers.
|
||||
*
|
||||
* @param npc the NPC to stop walking
|
||||
*/
|
||||
private void cancelWalking(@NotNull NPC npc)
|
||||
{
|
||||
isWalking = false;
|
||||
|
||||
List<Player> viewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
for(Player viewer : viewers)
|
||||
{
|
||||
if(npc.isWalking(viewer))
|
||||
npc.cancelWalking(viewer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the goal for storage.
|
||||
*
|
||||
* @param out the output stream
|
||||
* @throws IOException if serialization fails
|
||||
*/
|
||||
@Serial
|
||||
private void writeObject(@NotNull ObjectOutputStream out) throws IOException
|
||||
{
|
||||
out.defaultWriteObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the goal from storage.
|
||||
*
|
||||
* @param in the input stream
|
||||
* @throws IOException if deserialization fails
|
||||
* @throws ClassNotFoundException if class is not found
|
||||
*/
|
||||
@Serial
|
||||
private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException
|
||||
{
|
||||
in.defaultReadObject();
|
||||
this.targetLocation = serializableLocation.toLocation();
|
||||
}
|
||||
|
||||
// --- Builder Class ---
|
||||
|
||||
/**
|
||||
* Builder class for creating WalkToLocationGoal instances with a fluent API.
|
||||
*/
|
||||
public static class Builder
|
||||
{
|
||||
private final Location targetLocation;
|
||||
private double speed = DEFAULT_SPEED;
|
||||
private int maxIterations = DEFAULT_MAX_ITERATIONS;
|
||||
private boolean allowDiagonal = true;
|
||||
private SerializableConsumer<WalkingResult> completionCallback;
|
||||
private boolean withRotation = true;
|
||||
|
||||
/**
|
||||
* Creates a new Builder with the required target location.
|
||||
*
|
||||
* @param targetLocation The location to walk to
|
||||
*/
|
||||
public Builder(@NotNull Location targetLocation)
|
||||
{
|
||||
this.targetLocation = targetLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the walking speed.
|
||||
*
|
||||
* @param speed The walking speed (0.1 to 1.0)
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder speed(double speed)
|
||||
{
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum iterations for pathfinding.
|
||||
*
|
||||
* @param maxIterations Maximum iterations for pathfinding
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder maxIterations(int maxIterations)
|
||||
{
|
||||
this.maxIterations = maxIterations;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether diagonal movement is allowed.
|
||||
*
|
||||
* @param allowDiagonal Whether diagonal movement is allowed
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder allowDiagonal(boolean allowDiagonal)
|
||||
{
|
||||
this.allowDiagonal = allowDiagonal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completion callback.
|
||||
*
|
||||
* @param completionCallback Callback called when walking completes
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder completionCallback(@NotNull SerializableConsumer<WalkingResult> completionCallback)
|
||||
{
|
||||
this.completionCallback = completionCallback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether rotation packets should be sent.
|
||||
*
|
||||
* @param withRotation If true, includes rotation packets in the movement; otherwise only position packets are sent.
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder withRotation(boolean withRotation)
|
||||
{
|
||||
this.withRotation = withRotation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the WalkToLocationGoal instance.
|
||||
*
|
||||
* @return A new WalkToLocationGoal instance
|
||||
*/
|
||||
public WalkToLocationGoal build()
|
||||
{
|
||||
return new WalkToLocationGoal(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.utils.LocationUtils;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC wander randomly to nearby locations. The NPC will pick a random location within a specified radius and walk to it.
|
||||
*/
|
||||
public class WanderGoal extends Goal
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int DEFAULT_RADIUS = 10;
|
||||
public static final int DEFAULT_MIN_DELAY = 40; // 2 seconds
|
||||
public static final int DEFAULT_MAX_DELAY = 140; // 7 seconds
|
||||
|
||||
private int radius;
|
||||
private int minDelay;
|
||||
private int maxDelay;
|
||||
private double speed;
|
||||
|
||||
private transient WalkToLocationGoal currentWalkGoal;
|
||||
private transient int delayTicks;
|
||||
private transient Location targetLocation;
|
||||
|
||||
/**
|
||||
* Creates a WanderGoal with default settings.
|
||||
*/
|
||||
public WanderGoal()
|
||||
{
|
||||
this(DEFAULT_RADIUS, DEFAULT_MIN_DELAY, DEFAULT_MAX_DELAY, WalkToLocationGoal.DEFAULT_SPEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WanderGoal with custom radius and default delay.
|
||||
*
|
||||
* @param radius The maximum radius to wander (in blocks)
|
||||
*/
|
||||
public WanderGoal(int radius)
|
||||
{
|
||||
this(radius, DEFAULT_MIN_DELAY, DEFAULT_MAX_DELAY, WalkToLocationGoal.DEFAULT_SPEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WanderGoal with full configuration.
|
||||
*
|
||||
* @param radius The maximum radius to wander (in blocks)
|
||||
* @param minDelay Minimum delay between wander actions (in ticks)
|
||||
* @param maxDelay Maximum delay between wander actions (in ticks)
|
||||
* @param speed The walking speed
|
||||
*/
|
||||
public WanderGoal(int radius, int minDelay, int maxDelay, double speed)
|
||||
{
|
||||
super(Priority.MEDIUM);
|
||||
this.radius = radius;
|
||||
this.minDelay = minDelay;
|
||||
this.maxDelay = maxDelay;
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private WanderGoal(@NotNull WanderGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.radius = goal.radius;
|
||||
this.minDelay = goal.minDelay;
|
||||
this.maxDelay = goal.maxDelay;
|
||||
this.speed = goal.speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the radius for this goal.
|
||||
*
|
||||
* @return the radius
|
||||
*/
|
||||
public int getRadius()
|
||||
{
|
||||
return radius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the radius for this goal.
|
||||
*
|
||||
* @param radius the new radius
|
||||
*/
|
||||
public void setRadius(int radius)
|
||||
{
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum delay between wander actions.
|
||||
*
|
||||
* @return the maximum delay
|
||||
*/
|
||||
public int getMaxDelay()
|
||||
{
|
||||
return maxDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum delay between wander actions.
|
||||
*
|
||||
* @param maxDelay the new maximum delay
|
||||
*/
|
||||
public void setMaxDelay(int maxDelay)
|
||||
{
|
||||
this.maxDelay = maxDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum delay between wander actions.
|
||||
*
|
||||
* @return the minimum delay
|
||||
*/
|
||||
public int getMinDelay()
|
||||
{
|
||||
return minDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum delay between wander actions.
|
||||
*
|
||||
* @param minDelay the new minimum delay
|
||||
*/
|
||||
public void setMinDelay(int minDelay)
|
||||
{
|
||||
this.minDelay = minDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed for this goal.
|
||||
*
|
||||
* @return the speed
|
||||
*/
|
||||
public double getSpeed()
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speed for this goal.
|
||||
*
|
||||
* @param speed the new speed
|
||||
*/
|
||||
public void setSpeed(double speed)
|
||||
{
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true always (this goal can always be used)
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return radius >= 0 && minDelay >= 0 && maxDelay >= 0 && speed >= 0 && super.canUse(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the wander goal by picking a new target.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
pickNewTarget(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the wander goal, handling movement and delays.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
if(currentWalkGoal != null)
|
||||
{
|
||||
currentWalkGoal.tick(npc);
|
||||
|
||||
if(!currentWalkGoal.canContinue(npc))
|
||||
{
|
||||
currentWalkGoal.stop(npc);
|
||||
currentWalkGoal = null;
|
||||
delayTicks = minDelay + ThreadLocalRandom.current().nextInt(maxDelay - minDelay);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(delayTicks > 0)
|
||||
{
|
||||
delayTicks--;
|
||||
return;
|
||||
}
|
||||
|
||||
pickNewTarget(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the wander goal and cleans up state.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
if(currentWalkGoal != null)
|
||||
{
|
||||
currentWalkGoal.stop(npc);
|
||||
currentWalkGoal = null;
|
||||
}
|
||||
delayTicks = 0;
|
||||
targetLocation = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if a target location is set
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return targetLocation != null && super.canContinue(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be interrupted by a new goal selection.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the NPC is currently waiting (delayTicks == 1)
|
||||
*/
|
||||
@Override
|
||||
public boolean canBeInterrupted(@NotNull NPC npc)
|
||||
{
|
||||
return delayTicks == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new WanderGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks a new random target location near the NPC.
|
||||
*/
|
||||
private void pickNewTarget(@NotNull NPC npc)
|
||||
{
|
||||
Location currentLoc = npc.getLocation();
|
||||
World world = currentLoc.getWorld();
|
||||
|
||||
if(world == null)
|
||||
return;
|
||||
|
||||
int attempts = 0;
|
||||
while(attempts < 10)
|
||||
{
|
||||
double angle = ThreadLocalRandom.current().nextDouble() * Math.PI * 2;
|
||||
double distance = ThreadLocalRandom.current().nextDouble() * radius;
|
||||
|
||||
double x = currentLoc.getX() + Math.cos(angle) * distance;
|
||||
double z = currentLoc.getZ() + Math.sin(angle) * distance;
|
||||
|
||||
Location potentialLoc = new Location(world, x, currentLoc.getY(), z);
|
||||
OptionalInt y = LocationUtils.findSafeY(potentialLoc);
|
||||
|
||||
if(y.isPresent())
|
||||
{
|
||||
targetLocation = new Location(world, x, y.getAsInt(), z);
|
||||
currentWalkGoal = new WalkToLocationGoal.Builder(targetLocation).speed(speed).build();
|
||||
currentWalkGoal.start(npc);
|
||||
return;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
|
||||
delayTicks = maxDelay;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.eisi05.npc.api.enums;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Represents the visibility state of an NPC.
|
||||
*/
|
||||
public enum NpcVisibility implements Serializable
|
||||
{
|
||||
/**
|
||||
* The NPC is fully visible to the player.
|
||||
*/
|
||||
FULLY_VISIBLE,
|
||||
|
||||
/**
|
||||
* The NPC is transparent - the player can see through it, but it's still visible.
|
||||
*/
|
||||
TRANSPARENT,
|
||||
|
||||
/**
|
||||
* The NPC is completely invisible to the player.
|
||||
*/
|
||||
INVISIBLE;
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.eisi05.npc.api.enums;
|
||||
|
||||
public enum Result
|
||||
{
|
||||
SUCCESS,
|
||||
CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.eisi05.npc.api.enums;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
public enum WalkingResult implements Serializable
|
||||
{
|
||||
SUCCESS,
|
||||
CANCELLED;
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package de.eisi05.npc.api.events;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Called when an NPC is hidden from a player's view. This event is called after the NPC has been hidden from the player.
|
||||
*/
|
||||
public class NpcHideEvent extends Event
|
||||
{
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
private final Player player;
|
||||
private final NPC npc;
|
||||
|
||||
/**
|
||||
* Creates a new NpcHideEvent.
|
||||
*
|
||||
* @param player the player from whom the NPC is being hidden
|
||||
* @param npc the NPC that is being hidden
|
||||
* @throws IllegalArgumentException if player or npc is null
|
||||
*/
|
||||
public NpcHideEvent(@NotNull Player player, @NotNull NPC npc)
|
||||
{
|
||||
this.player = player;
|
||||
this.npc = npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for this event.
|
||||
*
|
||||
* @return the static HandlerList instance
|
||||
*/
|
||||
public static HandlerList getHandlerList()
|
||||
{
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers()
|
||||
{
|
||||
return getHandlerList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NPC that is being hidden.
|
||||
*
|
||||
* @return the NPC being hidden, never null
|
||||
*/
|
||||
public @NotNull NPC getNpc()
|
||||
{
|
||||
return npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player from whom the NPC is being hidden.
|
||||
*
|
||||
* @return the player, never null
|
||||
*/
|
||||
public @NotNull Player getPlayer()
|
||||
{
|
||||
return player;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Event triggered when a player interacts with an NPC.
|
||||
* Contains information about the player, the NPC, and the type of click action.
|
||||
* Event triggered when a player interacts with an NPC. Contains information about the player, the NPC, and the type of click action.
|
||||
*/
|
||||
public class NpcInteractEvent extends Event implements Serializable, Cancellable
|
||||
{
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package de.eisi05.npc.api.events;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Called after an NPC has been fully shown to a player and all packets have been sent. This event is called after the NPC is already visible to the player and
|
||||
* cannot be canceled.
|
||||
*/
|
||||
public class NpcPostShowEvent extends Event
|
||||
{
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
private final Player player;
|
||||
private final NPC npc;
|
||||
private final boolean wasViewer;
|
||||
|
||||
/**
|
||||
* Creates a new NpcPostShowEvent.
|
||||
*
|
||||
* @param player the player to whom the NPC was shown
|
||||
* @param npc the NPC that was shown
|
||||
* @param wasViewer whether the player was previously a viewer of this NPC
|
||||
* @throws IllegalArgumentException if player or npc is null
|
||||
*/
|
||||
public NpcPostShowEvent(@NotNull Player player, @NotNull NPC npc, boolean wasViewer)
|
||||
{
|
||||
this.player = player;
|
||||
this.npc = npc;
|
||||
this.wasViewer = wasViewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for this event.
|
||||
*
|
||||
* @return the static HandlerList instance
|
||||
*/
|
||||
public static HandlerList getHandlerList()
|
||||
{
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers()
|
||||
{
|
||||
return getHandlerList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NPC that was shown.
|
||||
*
|
||||
* @return the NPC that was shown, never null
|
||||
*/
|
||||
public @NotNull NPC getNpc()
|
||||
{
|
||||
return npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player to whom the NPC was shown.
|
||||
*
|
||||
* @return the player, never null
|
||||
*/
|
||||
public @NotNull Player getPlayer()
|
||||
{
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player was previously a viewer of this NPC. This can be used to determine if the player was seeing this NPC for the first time or if it was
|
||||
* a refresh of an existing view.
|
||||
*
|
||||
* @return true if the player was previously viewing this NPC, false otherwise
|
||||
*/
|
||||
public boolean wasViewer()
|
||||
{
|
||||
return wasViewer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package de.eisi05.npc.api.events;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Called when an NPC is shown to a player. This event is called before the NPC is shown to the player. The event can be canceled to prevent the NPC from being
|
||||
* shown.
|
||||
*/
|
||||
public class NpcPreShowEvent extends Event implements Cancellable
|
||||
{
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
private final Player player;
|
||||
private final NPC npc;
|
||||
private final boolean wasViewer;
|
||||
private boolean cancelled;
|
||||
|
||||
/**
|
||||
* Creates a new NpcPreShowEvent.
|
||||
*
|
||||
* @param player the player to whom the NPC is being shown
|
||||
* @param npc the NPC that is being shown
|
||||
* @param wasViewer whether the player was previously a viewer of this NPC
|
||||
* @throws IllegalArgumentException if player or npc is null
|
||||
*/
|
||||
public NpcPreShowEvent(@NotNull Player player, @NotNull NPC npc, boolean wasViewer)
|
||||
{
|
||||
this.player = player;
|
||||
this.npc = npc;
|
||||
this.wasViewer = wasViewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for this event.
|
||||
*
|
||||
* @return the static HandlerList instance
|
||||
*/
|
||||
public static HandlerList getHandlerList()
|
||||
{
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers()
|
||||
{
|
||||
return getHandlerList();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return true if this event is canceled
|
||||
*/
|
||||
@Override
|
||||
public boolean isCancelled()
|
||||
{
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param cancel true if you wish to cancel this event
|
||||
*/
|
||||
@Override
|
||||
public void setCancelled(boolean cancel)
|
||||
{
|
||||
cancelled = cancel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NPC that is being shown.
|
||||
*
|
||||
* @return the NPC being shown, never null
|
||||
*/
|
||||
public @NotNull NPC getNpc()
|
||||
{
|
||||
return npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player to whom the NPC is being shown.
|
||||
*
|
||||
* @return the player, never null
|
||||
*/
|
||||
public @NotNull Player getPlayer()
|
||||
{
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player was previously a viewer of this NPC. This can be used to determine if the player is seeing this NPC for the first time or if it's a
|
||||
* refresh of an existing view.
|
||||
*
|
||||
* @return true if the player was previously viewing this NPC, false otherwise
|
||||
*/
|
||||
public boolean wasViewer()
|
||||
{
|
||||
return wasViewer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package de.eisi05.npc.api.events;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Called when an NPC is about to begin walking along a specified {@link Path}.
|
||||
* <p>
|
||||
* This event is fired before the walking actually starts, allowing listeners to modify walking parameters such as speed or whether the NPC's real location
|
||||
* should be updated. The event can also be canceled to prevent the NPC from starting to walk.
|
||||
*/
|
||||
public class NpcStartWalkingEvent extends NpcWalkingEvent implements Cancellable
|
||||
{
|
||||
/**
|
||||
* The path the NPC will follow.
|
||||
*/
|
||||
private final Path path;
|
||||
|
||||
/**
|
||||
* The walking speed the NPC will use.
|
||||
*/
|
||||
private double walkSpeed;
|
||||
|
||||
/**
|
||||
* Whether the NPC's actual (server-side) location should be updated. If false, only the visual movement may be applied.
|
||||
*/
|
||||
private boolean changeRealLocation;
|
||||
|
||||
/**
|
||||
* Whether this event has been canceled.
|
||||
*/
|
||||
private boolean cancelled = false;
|
||||
|
||||
/**
|
||||
* Creates a new {@code NpcStartWalkingEvent}.
|
||||
*
|
||||
* @param npc the NPC that is about to start walking
|
||||
* @param path the path the NPC will follow
|
||||
* @param walkSpeed the speed at which the NPC will walk
|
||||
* @param changeRealLocation whether the NPC's real location should change during walking
|
||||
*/
|
||||
public NpcStartWalkingEvent(@NotNull NPC npc, @NotNull Path path, double walkSpeed, boolean changeRealLocation)
|
||||
{
|
||||
super(npc);
|
||||
this.path = path;
|
||||
this.walkSpeed = walkSpeed;
|
||||
this.changeRealLocation = changeRealLocation;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the path the NPC will follow.
|
||||
*
|
||||
* @return the walking path
|
||||
*/
|
||||
public @NotNull Path getPath()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NPC’s walking speed.
|
||||
*
|
||||
* @return the walking speed
|
||||
*/
|
||||
public double getWalkSpeed()
|
||||
{
|
||||
return walkSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the NPC’s walking speed.
|
||||
*
|
||||
* @param walkSpeed the new walking speed
|
||||
*/
|
||||
public void setWalkSpeed(double walkSpeed)
|
||||
{
|
||||
this.walkSpeed = walkSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the NPC's real location will be updated while walking.
|
||||
*
|
||||
* @return true if the real location changes, false otherwise
|
||||
*/
|
||||
public boolean isChangeRealLocation()
|
||||
{
|
||||
return changeRealLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the NPC's real location should update during walking.
|
||||
*
|
||||
* @param changeRealLocation true to update the real location
|
||||
*/
|
||||
public void setChangeRealLocation(boolean changeRealLocation)
|
||||
{
|
||||
this.changeRealLocation = changeRealLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isCancelled()
|
||||
{
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setCancelled(boolean cancel)
|
||||
{
|
||||
this.cancelled = cancel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.eisi05.npc.api.events;
|
||||
|
||||
import de.eisi05.npc.api.enums.WalkingResult;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Called when an NPC stops walking along its current path.
|
||||
* <p>
|
||||
* This event is fired after the walking process ends, either because the NPC successfully reached its destination ({@link WalkingResult#SUCCESS}) or because
|
||||
* the walk was canceled ({@link WalkingResult#CANCELLED}). Listeners may also control whether the NPC's real (server-side) location should be updated to the
|
||||
* NPC's final visual position.
|
||||
*/
|
||||
public class NpcStopWalkingEvent extends NpcWalkingEvent
|
||||
{
|
||||
private final WalkingResult walkingResult;
|
||||
private boolean changeRealLocation;
|
||||
|
||||
public NpcStopWalkingEvent(@NotNull NPC npc, @NotNull WalkingResult walkingResult, boolean changeRealLocation)
|
||||
{
|
||||
super(npc);
|
||||
this.walkingResult = walkingResult;
|
||||
this.changeRealLocation = changeRealLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the result of the walking process.
|
||||
* <p>
|
||||
* This may be:
|
||||
* <ul>
|
||||
* <li>{@link WalkingResult#SUCCESS} – the NPC reached its destination.</li>
|
||||
* <li>{@link WalkingResult#CANCELLED} – the walk was interrupted or stopped.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the walking result
|
||||
*/
|
||||
public @NotNull WalkingResult getWalkingResult()
|
||||
{
|
||||
return walkingResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the NPC's real location should be updated to its final walking position.
|
||||
*
|
||||
* @return true if the real location should change, false otherwise
|
||||
*/
|
||||
public boolean changeRealLocation()
|
||||
{
|
||||
return changeRealLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the NPC's real location should be updated when the walk ends.
|
||||
*
|
||||
* @param changeRealLocation true to apply the real location update
|
||||
*/
|
||||
public void setChangeRealLocation(boolean changeRealLocation)
|
||||
{
|
||||
this.changeRealLocation = changeRealLocation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.eisi05.npc.api.events;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an event that is called when an NPC's walking state changes. This is an abstract base class for NPC walking-related events.
|
||||
*/
|
||||
public abstract class NpcWalkingEvent extends Event
|
||||
{
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
private final NPC npc;
|
||||
|
||||
/**
|
||||
* Creates a new NpcWalkingEvent.
|
||||
*
|
||||
* @param npc the NPC whose walking state is changing
|
||||
* @throws IllegalArgumentException if npc is null
|
||||
*/
|
||||
public NpcWalkingEvent(@NotNull NPC npc)
|
||||
{
|
||||
this.npc = npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for this event.
|
||||
*
|
||||
* @return the static HandlerList instance
|
||||
*/
|
||||
public static HandlerList getHandlerList()
|
||||
{
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the NPC involved in this event.
|
||||
*
|
||||
* @return the interacted NPC, never null
|
||||
*/
|
||||
public @NotNull NPC getNpc()
|
||||
{
|
||||
return npc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers()
|
||||
{
|
||||
return getHandlerList();
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Functional interface representing an action to be performed
|
||||
* when an NPC is clicked.
|
||||
* Functional interface representing an action to be performed when an NPC is clicked.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface NpcClickAction extends Serializable
|
||||
@@ -24,19 +23,15 @@ public interface NpcClickAction extends Serializable
|
||||
void call(@NotNull NpcInteractEvent event);
|
||||
|
||||
/**
|
||||
* Returns a copy of this NpcClickAction.
|
||||
* The default implementation returns the same instance.
|
||||
* Returns a copy of this NpcClickAction. The default implementation returns the same instance.
|
||||
*
|
||||
* @return a copy of this action
|
||||
*/
|
||||
default NpcClickAction copy() {return this;}
|
||||
|
||||
/**
|
||||
* Initializes this {@link NpcClickAction}.
|
||||
* This method can be used for any setup or configuration that needs to occur
|
||||
* after the action is created or loaded.
|
||||
* The default implementation simply returns the current instance,
|
||||
* indicating no specific initialization is required by default.
|
||||
* Initializes this {@link NpcClickAction}. This method can be used for any setup or configuration that needs to occur after the action is created or
|
||||
* loaded. The default implementation simply returns the current instance, indicating no specific initialization is required by default.
|
||||
*
|
||||
* @return The initialized {@link NpcClickAction} instance. By default, it returns {@code this}.
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.eisi05.npc.api.listeners;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedPlayerTeam;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
@@ -13,14 +12,22 @@ public class ChangeWorldListener implements Listener
|
||||
@EventHandler
|
||||
public void onChange(PlayerChangedWorldEvent event)
|
||||
{
|
||||
WrappedPlayerTeam.clear(event.getPlayer().getUniqueId());
|
||||
if(!NpcApi.config.autoManageVisibility())
|
||||
return;
|
||||
|
||||
new BukkitRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
NpcManager.getList().forEach(npc -> npc.showNPCToPlayer(event.getPlayer()));
|
||||
NpcManager.getList().forEach(npc ->
|
||||
{
|
||||
if(npc.getVisibilityManager().shouldShowToPlayer(event.getPlayer().getUniqueId()))
|
||||
{
|
||||
npc.showNPCToPlayer(event.getPlayer());
|
||||
npc.addWalkingViewer(event.getPlayer());
|
||||
}
|
||||
});
|
||||
}
|
||||
}.runTaskLater(NpcApi.plugin, 10L);
|
||||
}
|
||||
|
||||
@@ -3,29 +3,50 @@ package de.eisi05.npc.api.listeners;
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.objects.NpcSkin;
|
||||
import de.eisi05.npc.api.scheduler.Tasks;
|
||||
import de.eisi05.npc.api.utils.PacketReader;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedPlayerTeam;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ConnectionListener implements Listener
|
||||
{
|
||||
@EventHandler
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onJoin(PlayerJoinEvent event)
|
||||
{
|
||||
PacketReader.inject(event.getPlayer());
|
||||
|
||||
WrappedPlayerTeam.clear(event.getPlayer().getUniqueId());
|
||||
|
||||
if(!NpcApi.config.autoManageVisibility())
|
||||
return;
|
||||
|
||||
new BukkitRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
NpcManager.getList().forEach(npc -> npc.showNPCToPlayer(event.getPlayer()));
|
||||
for(NPC npc : new ArrayList<>(NpcManager.getList()))
|
||||
{
|
||||
if(!npc.getVisibilityManager().shouldShowToPlayer(event.getPlayer().getUniqueId()))
|
||||
continue;
|
||||
|
||||
npc.showNPCToPlayer(event.getPlayer());
|
||||
npc.addWalkingViewer(event.getPlayer());
|
||||
NpcSkin npcSkin = npc.getOption(NpcOption.SKIN, event.getPlayer());
|
||||
if(npcSkin == null || npcSkin.isStatic() || npcSkin.getPlaceholder() == null || npc.getOption(NpcOption.USE_PLAYER_SKIN, event.getPlayer()))
|
||||
continue;
|
||||
|
||||
Tasks.updateSkin(event.getPlayer(), npc, npcSkin);
|
||||
}
|
||||
}
|
||||
}.runTaskLater(NpcApi.plugin, 10L);
|
||||
}
|
||||
@@ -35,7 +56,15 @@ public class ConnectionListener implements Listener
|
||||
{
|
||||
PacketReader.uninject(event.getPlayer());
|
||||
|
||||
Tasks.placeholderCache.remove(event.getPlayer().getUniqueId());
|
||||
|
||||
for(NPC npc : NpcManager.getList())
|
||||
npc.hideNpcFromPlayer(event.getPlayer());
|
||||
{
|
||||
npc.nameCache.remove(event.getPlayer().getUniqueId());
|
||||
npc.removeWalkingViewer(event.getPlayer());
|
||||
|
||||
if(NpcApi.config.autoManageVisibility())
|
||||
npc.hideNpcFromPlayer(event.getPlayer());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.eisi05.npc.api.listeners;
|
||||
|
||||
import de.eisi05.npc.api.enums.ClickActionType;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.utils.NpcHitboxUtil;
|
||||
import de.eisi05.npc.api.utils.PacketReader;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Pose;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class NpcSleepListener implements Listener
|
||||
{
|
||||
private static boolean checkForRay(@NotNull Player player, @NotNull ClickActionType type)
|
||||
{
|
||||
double rangeSq = 25.0;
|
||||
Location eye = player.getEyeLocation();
|
||||
World world = eye.getWorld();
|
||||
|
||||
for(NPC npc : NpcManager.getList())
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
|
||||
if(npcLoc.getWorld() != world)
|
||||
continue;
|
||||
|
||||
if(npc.getOption(NpcOption.POSE, player) != Pose.SLEEPING)
|
||||
continue;
|
||||
|
||||
if(npcLoc.distanceSquared(eye) > rangeSq)
|
||||
continue;
|
||||
|
||||
if(NpcHitboxUtil.rayIntersectsNpc(npc, true, player))
|
||||
{
|
||||
PacketReader.callNpc(player, npc, type);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInteract(PlayerInteractEvent event)
|
||||
{
|
||||
if(event.getHand() == EquipmentSlot.OFF_HAND)
|
||||
return;
|
||||
|
||||
Action action = event.getAction();
|
||||
Player player = event.getPlayer();
|
||||
if(action == Action.RIGHT_CLICK_BLOCK || action == Action.RIGHT_CLICK_AIR)
|
||||
event.setCancelled(checkForRay(player, ClickActionType.RIGHT));
|
||||
|
||||
if(action == Action.LEFT_CLICK_BLOCK || action == Action.LEFT_CLICK_AIR)
|
||||
event.setCancelled(checkForRay(player, ClickActionType.LEFT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.eisi05.npc.api.listeners;
|
||||
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import org.bukkit.entity.AbstractArrow;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.ProjectileHitEvent;
|
||||
|
||||
public class ProjectileHitListener implements Listener
|
||||
{
|
||||
@EventHandler
|
||||
public void onProjectileHit(ProjectileHitEvent event)
|
||||
{
|
||||
if(!(event.getEntity() instanceof AbstractArrow arrow))
|
||||
return;
|
||||
|
||||
if(!(arrow.getShooter() instanceof Entity entity))
|
||||
return;
|
||||
|
||||
if(NpcManager.fromId(entity.getEntityId()).isPresent())
|
||||
arrow.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.eisi05.npc.api.listeners;
|
||||
|
||||
import de.eisi05.npc.api.utils.PacketReader;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.server.ServerLoadEvent;
|
||||
|
||||
public class ServerReadyListener implements Listener
|
||||
{
|
||||
@EventHandler
|
||||
public void onServerReady(ServerLoadEvent event)
|
||||
{
|
||||
PacketReader.injectAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.eisi05.npc.api.listeners;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class WorldLoadListener implements Listener
|
||||
{
|
||||
@EventHandler
|
||||
public void onWorldLoad(WorldLoadEvent event)
|
||||
{
|
||||
NpcManager.loadWorld(event.getWorld());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onChunkLoad(ChunkLoadEvent event)
|
||||
{
|
||||
if(!NpcApi.config.autoManageVisibility())
|
||||
return;
|
||||
|
||||
int chunkX = event.getChunk().getX();
|
||||
int chunkZ = event.getChunk().getZ();
|
||||
|
||||
Collection<Player> players = event.getWorld().getPlayers();
|
||||
new ArrayList<>(NpcManager.getList())
|
||||
.stream()
|
||||
.filter(npc -> npc.getLocation().getWorld().getUID().equals(event.getWorld().getUID()))
|
||||
.filter(npc -> (npc.getLocation().getBlockX() >> 4) == chunkX && ((npc.getLocation().getBlockZ() >> 4) == chunkZ))
|
||||
.forEach(npc ->
|
||||
players.forEach(player ->
|
||||
{
|
||||
if(npc.getVisibilityManager().shouldShowToPlayer(player.getUniqueId()))
|
||||
{
|
||||
npc.showNPCToPlayer(player);
|
||||
npc.addWalkingViewer(player);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onChunkUnload(ChunkUnloadEvent event)
|
||||
{
|
||||
if(!NpcApi.config.autoManageVisibility())
|
||||
return;
|
||||
|
||||
int chunkX = event.getChunk().getX();
|
||||
int chunkZ = event.getChunk().getZ();
|
||||
|
||||
Collection<Player> players = event.getWorld().getPlayers();
|
||||
new ArrayList<>(NpcManager.getList())
|
||||
.stream()
|
||||
.filter(npc -> npc.getLocation().getWorld().getUID().equals(event.getWorld().getUID()))
|
||||
.filter(npc -> (npc.getLocation().getBlockX() >> 4) == chunkX && ((npc.getLocation().getBlockZ() >> 4) == chunkZ))
|
||||
.forEach(npc -> players.forEach(npc::hideNpcFromPlayer));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package de.eisi05.npc.api.manager;
|
||||
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.utils.ObjectSaver;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.*;
|
||||
@@ -14,13 +20,19 @@ import java.util.*;
|
||||
*/
|
||||
public class NpcManager
|
||||
{
|
||||
/**
|
||||
* Stores serialized NPCs that should be loaded once their world becomes available. The key is the world UUID, and the value is a list of NPCs waiting to be
|
||||
* deserialized.
|
||||
*/
|
||||
private static final Map<UUID, List<NPC.SerializedNPC>> toLoadNPCs = new HashMap<>();
|
||||
|
||||
private static final Map<Integer, NPC> npcById = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Map storing the file name and the exception that occurred during loading.
|
||||
*/
|
||||
public static Map<String, Exception> loadExceptions = new HashMap<>();
|
||||
|
||||
private static final List<NPC> listNPC = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds an NPC to the manager's list.
|
||||
*
|
||||
@@ -28,17 +40,17 @@ public class NpcManager
|
||||
*/
|
||||
public static void addNPC(@NotNull NPC npc)
|
||||
{
|
||||
listNPC.add(npc);
|
||||
npcById.put(npc.getServerPlayer().getId(), npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of all managed NPCs.
|
||||
* Returns the set of all managed NPCs.
|
||||
*
|
||||
* @return the list of NPCs
|
||||
* @return the set of NPCs
|
||||
*/
|
||||
public static @NotNull List<NPC> getList()
|
||||
public static @NotNull Collection<NPC> getList()
|
||||
{
|
||||
return listNPC;
|
||||
return npcById.values();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +60,13 @@ public class NpcManager
|
||||
*/
|
||||
public static void removeNPC(@NotNull NPC npc)
|
||||
{
|
||||
listNPC.remove(npc);
|
||||
npcById.remove(npc.getServerPlayer().getId());
|
||||
npcById.remove(npc.entity.getId());
|
||||
}
|
||||
|
||||
public static void addID(int id, @NotNull NPC npc)
|
||||
{
|
||||
npcById.put(id, npc);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +74,7 @@ public class NpcManager
|
||||
*/
|
||||
public static void clear()
|
||||
{
|
||||
listNPC.clear();
|
||||
npcById.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,12 +85,32 @@ public class NpcManager
|
||||
*/
|
||||
public static @NotNull Optional<NPC> fromUUID(@NotNull UUID uuid)
|
||||
{
|
||||
return listNPC.stream().filter(npc -> npc.getUUID().equals(uuid)).findFirst();
|
||||
return getList().stream().filter(npc -> npc.getUUID().equals(uuid)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads NPCs from disk files in the plugin data folder.
|
||||
* Logs the count of successfully and unsuccessfully loaded NPCs.
|
||||
* Finds an NPC by its entity ID.
|
||||
*
|
||||
* @param id the entity ID to search for
|
||||
* @return an Optional containing the NPC if found, empty otherwise
|
||||
*/
|
||||
public static @Nullable Optional<NPC> fromId(int id)
|
||||
{
|
||||
return Optional.ofNullable(npcById.get(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flattened list of all serialized NPCs that are scheduled to be loaded.
|
||||
*
|
||||
* @return a non-null list containing all {@link NPC.SerializedNPC} instances across all worlds
|
||||
*/
|
||||
public static @NotNull List<NPC.SerializedNPC> getToLoadNPCs()
|
||||
{
|
||||
return toLoadNPCs.values().stream().flatMap(Collection::stream).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads NPCs from disk files in the plugin data folder. Logs the count of successfully and unsuccessfully loaded NPCs.
|
||||
*/
|
||||
public static void loadNPCs()
|
||||
{
|
||||
@@ -94,17 +132,21 @@ public class NpcManager
|
||||
try
|
||||
{
|
||||
NPC.SerializedNPC serializedNPC = new ObjectSaver(file1).read();
|
||||
NPC npc = serializedNPC.deserializedNPC();
|
||||
Either<NPC, UUID> npcEither = serializedNPC.deserializedNPC();
|
||||
|
||||
LocalDate date = LocalDate.of(2025, 10, 22);
|
||||
LocalTime time = LocalTime.of(22, 0);
|
||||
Instant instant = LocalDateTime.of(date, time).atZone(ZoneId.of("UTC")).toInstant();
|
||||
if(npc.getCreatedAt().isBefore(instant))
|
||||
npc.setEditable(true);
|
||||
if(npcEither.right().isPresent())
|
||||
{
|
||||
toLoadNPCs.computeIfAbsent(npcEither.right().get(), k -> new ArrayList<>()).add(serializedNPC);
|
||||
continue;
|
||||
}
|
||||
|
||||
npc.showNpcToAllPlayers();
|
||||
if(npcEither.left().isEmpty())
|
||||
continue;
|
||||
|
||||
loadNpc(npcEither.left().get());
|
||||
successCounter++;
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
failCounter++;
|
||||
exception = e;
|
||||
@@ -125,4 +167,108 @@ public class NpcManager
|
||||
if(exception != null && NpcApi.config.debug())
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all serialized NPCs that were queued for the given world and initializes them.
|
||||
*
|
||||
* @param world the world whose queued NPCs should be deserialized and spawned
|
||||
*/
|
||||
public static void loadWorld(@NotNull World world)
|
||||
{
|
||||
List<NPC.SerializedNPC> serializedNPCS = toLoadNPCs.remove(world.getUID());
|
||||
|
||||
if(serializedNPCS == null || serializedNPCS.isEmpty())
|
||||
return;
|
||||
|
||||
Exception exception = null;
|
||||
for(NPC.SerializedNPC serializedNPC : serializedNPCS)
|
||||
{
|
||||
try
|
||||
{
|
||||
Either<NPC, ?> either = serializedNPC.deserializedNPC();
|
||||
|
||||
if(either.left().isEmpty())
|
||||
continue;
|
||||
|
||||
loadNpc(either.left().get());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
exception = e;
|
||||
}
|
||||
}
|
||||
|
||||
if(exception != null && NpcApi.config.debug())
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load the given serialized NPC, optionally overriding its location.
|
||||
* <p>
|
||||
* The NPC is removed from the internal loading map before attempting deserialization. If a non-null location is provided, it will replace the NPC's stored
|
||||
* location. If deserialization succeeds, the NPC is loaded and {@code true} is returned. Otherwise, {@code false} is returned.
|
||||
* <p>
|
||||
* Any exceptions during deserialization are caught and optionally printed if debug mode is enabled.
|
||||
*
|
||||
* @param serializedNPC the serialized NPC to load, must not be null
|
||||
* @param location an optional location to override the NPC's position, may be null
|
||||
* @return {@code true} if the NPC was successfully loaded, otherwise {@code false}
|
||||
*/
|
||||
public static boolean loadSerializedNPC(@NotNull NPC.SerializedNPC serializedNPC, @Nullable Location location)
|
||||
{
|
||||
toLoadNPCs.computeIfPresent(
|
||||
serializedNPC.getWorld(),
|
||||
(k, list) ->
|
||||
{
|
||||
list.remove(serializedNPC);
|
||||
return list.isEmpty() ? null : list;
|
||||
}
|
||||
);
|
||||
|
||||
if(location != null)
|
||||
serializedNPC.setLocation(location);
|
||||
|
||||
try
|
||||
{
|
||||
Either<NPC, ?> either = serializedNPC.deserializedNPC();
|
||||
|
||||
if(either.left().isEmpty())
|
||||
return false;
|
||||
|
||||
loadNpc(either.left().get());
|
||||
return true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if(NpcApi.config.debug())
|
||||
e.printStackTrace();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the given NPC, applying editability rules based on its creation time and making it visible to all online players.
|
||||
*
|
||||
* @param npc the NPC to load and display
|
||||
*/
|
||||
private static void loadNpc(@NotNull NPC npc)
|
||||
{
|
||||
LocalDate date = LocalDate.of(2025, 10, 22);
|
||||
LocalTime time = LocalTime.of(22, 0);
|
||||
Instant instant = LocalDateTime.of(date, time).atZone(ZoneId.of("UTC")).toInstant();
|
||||
if(npc.getCreatedAt().isBefore(instant))
|
||||
npc.setEditable(true);
|
||||
|
||||
if(npc.getVisibilityManager().shouldShowToAllPlayers())
|
||||
npc.showNpcToAllPlayers();
|
||||
else
|
||||
npc.getVisibilityManager().getSpecificPlayers().forEach(uuid ->
|
||||
{
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player == null)
|
||||
return;
|
||||
npc.showNPCToPlayer(player);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package de.eisi05.npc.api.manager;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages visibility settings for an NPC, controlling whether it should be shown to all players or only to specific players.
|
||||
*/
|
||||
public class NpcVisibilityManager implements Serializable
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Set<UUID> specificPlayers = new HashSet<>();
|
||||
private boolean showToAllPlayers = true;
|
||||
|
||||
/**
|
||||
* Sets whether this NPC should be shown to all players (including new ones joining).
|
||||
*
|
||||
* @param showToAllPlayers true if the NPC should be shown to all players, false if only to specific players
|
||||
*/
|
||||
public void setShowToAllPlayers(boolean showToAllPlayers)
|
||||
{
|
||||
this.showToAllPlayers = showToAllPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this NPC should be shown to all players.
|
||||
*
|
||||
* @return true if the NPC should be shown to all players, false if only to specific players
|
||||
*/
|
||||
public boolean shouldShowToAllPlayers()
|
||||
{
|
||||
return showToAllPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a player to the list of specific players who should see this NPC. This has no effect if showToAllPlayers is true.
|
||||
*
|
||||
* @param playerUuid the UUID of the player to add
|
||||
* @return true if the player was added, false if the player was already in the list
|
||||
*/
|
||||
public boolean addSpecificPlayer(@NotNull UUID playerUuid)
|
||||
{
|
||||
return specificPlayers.add(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a player from the list of specific players who should see this NPC.
|
||||
*
|
||||
* @param playerUuid the UUID of the player to remove
|
||||
* @return true if the player was removed, false if the player was not in the list
|
||||
*/
|
||||
public boolean removeSpecificPlayer(@NotNull UUID playerUuid)
|
||||
{
|
||||
return specificPlayers.remove(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific player should see this NPC.
|
||||
*
|
||||
* @param playerUuid the UUID of the player to check
|
||||
* @return true if the player should see this NPC, false otherwise
|
||||
*/
|
||||
public boolean shouldShowToPlayer(@NotNull UUID playerUuid)
|
||||
{
|
||||
return showToAllPlayers || specificPlayers.contains(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the set of specific players who should see this NPC.
|
||||
*
|
||||
* @return an immutable set of player UUIDs
|
||||
*/
|
||||
public @NotNull Set<UUID> getSpecificPlayers()
|
||||
{
|
||||
return Collections.unmodifiableSet(specificPlayers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all specific players from the list.
|
||||
*/
|
||||
public void clearSpecificPlayers()
|
||||
{
|
||||
specificPlayers.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package de.eisi05.npc.api.movement;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a single movement data point containing position, rotation, and timing information.
|
||||
* This class is used to store individual frames of player movement for later replay.
|
||||
*/
|
||||
public class MovementData implements Serializable
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final long timestamp;
|
||||
private final double x, y, z;
|
||||
private final float yaw, pitch;
|
||||
private final UUID worldUUID;
|
||||
|
||||
/**
|
||||
* Creates a new MovementData instance from a location and timestamp.
|
||||
*
|
||||
* @param location The location to record
|
||||
* @param timestamp The timestamp in milliseconds when this movement occurred
|
||||
*/
|
||||
public MovementData(@NotNull Location location, long timestamp)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.x = location.getX();
|
||||
this.y = location.getY();
|
||||
this.z = location.getZ();
|
||||
this.yaw = location.getYaw();
|
||||
this.pitch = location.getPitch();
|
||||
this.worldUUID = location.getWorld().getUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp when this movement occurred.
|
||||
*
|
||||
* @return The timestamp in milliseconds
|
||||
*/
|
||||
public long getTimestamp()
|
||||
{
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the X coordinate.
|
||||
*
|
||||
* @return The X coordinate
|
||||
*/
|
||||
public double getX()
|
||||
{
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Y coordinate.
|
||||
*
|
||||
* @return The Y coordinate
|
||||
*/
|
||||
public double getY()
|
||||
{
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Z coordinate.
|
||||
*
|
||||
* @return The Z coordinate
|
||||
*/
|
||||
public double getZ()
|
||||
{
|
||||
return z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the yaw rotation.
|
||||
*
|
||||
* @return The yaw in degrees
|
||||
*/
|
||||
public float getYaw()
|
||||
{
|
||||
return yaw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pitch rotation.
|
||||
*
|
||||
* @return The pitch in degrees
|
||||
*/
|
||||
public float getPitch()
|
||||
{
|
||||
return pitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the world uuid where this movement occurred.
|
||||
*
|
||||
* @return The world uuid
|
||||
*/
|
||||
public @NotNull UUID getWorldUUID()
|
||||
{
|
||||
return worldUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Location object from this movement data.
|
||||
*
|
||||
* @param world The world to create the location in
|
||||
* @return A Location object representing this movement data
|
||||
*/
|
||||
public @NotNull Location toLocation(@NotNull World world)
|
||||
{
|
||||
return new Location(world, x, y, z, yaw, pitch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time difference between this movement and another.
|
||||
*
|
||||
* @param other The other movement data
|
||||
* @return The time difference in milliseconds
|
||||
*/
|
||||
public long getTimeDifference(@NotNull MovementData other)
|
||||
{
|
||||
return Math.abs(this.timestamp - other.timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString()
|
||||
{
|
||||
return String.format("MovementData{world='%s', x=%.2f, y=%.2f, z=%.2f, yaw=%.1f, pitch=%.1f, time=%d}", worldUUID, x, y, z, yaw, pitch, timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package de.eisi05.npc.api.movement;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Records player movements at a specified interval using a scheduler-based approach.
|
||||
* This provides more precise and consistent tracking compared to PlayerMoveEvent.
|
||||
*/
|
||||
public class MovementRecorder
|
||||
{
|
||||
private static final ConcurrentHashMap<UUID, RecordingSession> activeRecordings = new ConcurrentHashMap<>();
|
||||
private static final AtomicLong sessionIdCounter = new AtomicLong(0);
|
||||
|
||||
/**
|
||||
* Starts recording a player's movements.
|
||||
*
|
||||
* @param player The player to record
|
||||
* @param intervalTicks The recording interval in ticks (1 tick = 50ms)
|
||||
* @return The recording session ID
|
||||
*/
|
||||
public static long startRecording(@NotNull Player player, int intervalTicks)
|
||||
{
|
||||
long sessionId = sessionIdCounter.incrementAndGet();
|
||||
RecordingSession session = new RecordingSession(player, sessionId, intervalTicks);
|
||||
activeRecordings.put(player.getUniqueId(), session);
|
||||
session.start();
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops recording a player's movements and returns the recorded data.
|
||||
*
|
||||
* @param player The player to stop recording
|
||||
* @return The recorded movement data, or null if no active recording
|
||||
*/
|
||||
public static @Nullable MovementRecording stopRecording(@NotNull Player player)
|
||||
{
|
||||
RecordingSession session = activeRecordings.remove(player.getUniqueId());
|
||||
if (session != null)
|
||||
return session.stop();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops recording by session ID and returns the recorded data.
|
||||
*
|
||||
* @param sessionId The session ID to stop
|
||||
* @return The recorded movement data, or null if session not found
|
||||
*/
|
||||
public static @Nullable MovementRecording stopRecording(long sessionId)
|
||||
{
|
||||
RecordingSession session = activeRecordings.values().stream()
|
||||
.filter(s -> s.getSessionId() == sessionId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
activeRecordings.remove(session.getPlayer().getUniqueId());
|
||||
return session.stop();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player is currently being recorded.
|
||||
*
|
||||
* @param player The player to check
|
||||
* @return true if actively recording, false otherwise
|
||||
*/
|
||||
public static boolean isRecording(@NotNull Player player)
|
||||
{
|
||||
return activeRecordings.containsKey(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active recording session for a player.
|
||||
*
|
||||
* @param player The player
|
||||
* @return The recording session, or null if not recording
|
||||
*/
|
||||
public static @Nullable RecordingSession getActiveSession(@NotNull Player player)
|
||||
{
|
||||
return activeRecordings.get(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an active recording session for a player.
|
||||
*/
|
||||
public static class RecordingSession
|
||||
{
|
||||
private final Player player;
|
||||
private final long sessionId;
|
||||
private final int intervalTicks;
|
||||
private final ArrayList<MovementData> movements;
|
||||
private final long startTime;
|
||||
private BukkitTask recordingTask;
|
||||
|
||||
private RecordingSession(@NotNull Player player, long sessionId, int intervalTicks)
|
||||
{
|
||||
this.player = player;
|
||||
this.sessionId = sessionId;
|
||||
this.intervalTicks = Math.max(1, intervalTicks);
|
||||
this.movements = new ArrayList<>();
|
||||
this.startTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void start()
|
||||
{
|
||||
movements.add(new MovementData(player.getLocation(), 0));
|
||||
|
||||
recordingTask = new BukkitRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if (!player.isOnline())
|
||||
{
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
long timestamp = System.currentTimeMillis() - startTime;
|
||||
movements.add(new MovementData(player.getLocation(), timestamp));
|
||||
}
|
||||
}.runTaskTimer(NpcApi.plugin, intervalTicks, intervalTicks);
|
||||
}
|
||||
|
||||
private @NotNull MovementRecording stop()
|
||||
{
|
||||
if (recordingTask != null)
|
||||
{
|
||||
recordingTask.cancel();
|
||||
recordingTask = null;
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
return new MovementRecording(movements, sessionId, startTime, endTime, player.getUniqueId(), intervalTicks);
|
||||
}
|
||||
|
||||
public @NotNull Player getPlayer()
|
||||
{
|
||||
return player;
|
||||
}
|
||||
|
||||
public long getSessionId()
|
||||
{
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public int getIntervalTicks()
|
||||
{
|
||||
return intervalTicks;
|
||||
}
|
||||
|
||||
public @NotNull List<MovementData> getMovements()
|
||||
{
|
||||
return new ArrayList<>(movements);
|
||||
}
|
||||
|
||||
public long getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public long getDuration()
|
||||
{
|
||||
return System.currentTimeMillis() - startTime;
|
||||
}
|
||||
|
||||
public int getMovementCount()
|
||||
{
|
||||
return movements.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package de.eisi05.npc.api.movement;
|
||||
|
||||
import de.eisi05.npc.api.utils.ObjectSaver;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Creates a new MovementRecording instance.
|
||||
*
|
||||
* @param movements The list of movement data points
|
||||
* @param sessionId The recording session ID
|
||||
* @param startTime The recording start time in milliseconds
|
||||
* @param endTime The recording end time in milliseconds
|
||||
* @param playerUUID The UUID of the recorded player
|
||||
* @param intervalTicks The recording interval in ticks
|
||||
*/
|
||||
public record MovementRecording(@NotNull ArrayList<MovementData> movements, long sessionId, long startTime, long endTime, @NotNull UUID playerUUID,
|
||||
int intervalTicks) implements Serializable
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Loads a MovementRecording from a file.
|
||||
*
|
||||
* @param file The file to load the recording from
|
||||
* @return The loaded MovementRecording
|
||||
* @throws IOException If an I/O error occurs while reading the file
|
||||
* @throws RuntimeException If the file cannot be deserialized
|
||||
*/
|
||||
public static @NotNull MovementRecording loadFromFile(@NotNull File file) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
return new ObjectSaver(file).read();
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
if(e.getCause() instanceof IOException ioException)
|
||||
throw ioException;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this MovementRecording to a file.
|
||||
*
|
||||
* @param file The file to save the recording to
|
||||
* @throws IOException If an I/O error occurs while writing the file
|
||||
*/
|
||||
public void saveToFile(@NotNull File file) throws IOException
|
||||
{
|
||||
new ObjectSaver(file).write(this, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total duration of the recording.
|
||||
*
|
||||
* @return The duration in milliseconds
|
||||
*/
|
||||
public long getDuration()
|
||||
{
|
||||
return endTime - startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of movement data points in this recording.
|
||||
*
|
||||
* @return The movement count
|
||||
*/
|
||||
public int getMovementCount()
|
||||
{
|
||||
return movements.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first movement data point.
|
||||
*
|
||||
* @return The first movement, or null if empty
|
||||
*/
|
||||
public @Nullable MovementData getFirstMovement()
|
||||
{
|
||||
return movements.isEmpty() ? null : movements.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last movement data point.
|
||||
*
|
||||
* @return The last movement, or null if empty
|
||||
*/
|
||||
public @Nullable MovementData getLastMovement()
|
||||
{
|
||||
return movements.isEmpty() ? null : movements.get(movements.size() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString()
|
||||
{
|
||||
return String.format("MovementRecording{sessionId=%d, movements=%d, duration=%dms, player=%s}",
|
||||
sessionId, movements.size(), getDuration(), playerUUID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package de.eisi05.npc.api.movement;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.wrapper.packets.RotateHeadPacket;
|
||||
import de.eisi05.npc.api.wrapper.packets.TeleportEntityPacket;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Replays recorded movements on NPCs with precise timing and smooth transitions. Supports different replay modes and provides callbacks for completion.
|
||||
*/
|
||||
public class MovementReplayer
|
||||
{
|
||||
private static final ConcurrentHashMap<UUID, ReplaySession> activeReplays = new ConcurrentHashMap<>();
|
||||
private static final AtomicLong replayIdCounter = new AtomicLong(0);
|
||||
|
||||
/**
|
||||
* Starts replaying recorded movements on an NPC.
|
||||
*
|
||||
* @param npc The NPC to control
|
||||
* @param recording The movement recording
|
||||
* @param speedMultiplier Speed multiplier (speed up or slows down the replay), normal speed = 1
|
||||
* @param changeRealLocation Whether to change the real location of the NPC
|
||||
* @param onComplete Callback when replay completes
|
||||
* @return The replay session ID
|
||||
*/
|
||||
public static long startReplay(@NotNull NPC npc, @NotNull MovementRecording recording, double speedMultiplier, boolean changeRealLocation,
|
||||
@Nullable Consumer<ReplayResult> onComplete, @NotNull Player... viewers)
|
||||
{
|
||||
if(recording.movements().isEmpty())
|
||||
{
|
||||
if(onComplete != null)
|
||||
onComplete.accept(ReplayResult.COMPLETED);
|
||||
return -1;
|
||||
}
|
||||
|
||||
long replayId = replayIdCounter.incrementAndGet();
|
||||
ReplaySession session = new ReplaySession(npc, recording, speedMultiplier, changeRealLocation, onComplete, replayId, viewers);
|
||||
|
||||
session.start();
|
||||
return replayId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops an active replay session.
|
||||
*
|
||||
* @param npc The NPC whose replay to stop
|
||||
* @return true if a replay was stopped, false otherwise
|
||||
*/
|
||||
public static boolean stopReplay(@NotNull NPC npc)
|
||||
{
|
||||
ReplaySession session = activeReplays.remove(npc.getUUID());
|
||||
if(session != null)
|
||||
{
|
||||
session.stop();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a replay by session ID.
|
||||
*
|
||||
* @param replayId The replay session ID
|
||||
* @return true if a replay was stopped, false otherwise
|
||||
*/
|
||||
public static boolean stopReplay(long replayId)
|
||||
{
|
||||
ReplaySession session = activeReplays.values().stream()
|
||||
.filter(s -> s.getReplayId() == replayId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if(session != null)
|
||||
{
|
||||
if(session.getNpc() != null)
|
||||
activeReplays.remove(session.getNpc().getUUID());
|
||||
session.stop();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an NPC is currently replaying movements.
|
||||
*
|
||||
* @param npc The NPC to check
|
||||
* @return true if actively replaying, false otherwise
|
||||
*/
|
||||
public static boolean isReplaying(@NotNull NPC npc)
|
||||
{
|
||||
return activeReplays.containsKey(npc.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the result of a replay session.
|
||||
*/
|
||||
public enum ReplayResult
|
||||
{
|
||||
COMPLETED,
|
||||
CANCELLED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an active replay session for an NPC.
|
||||
*/
|
||||
private static class ReplaySession
|
||||
{
|
||||
private final NPC npc;
|
||||
private final MovementRecording recording;
|
||||
private final double speedMultiplier;
|
||||
private final boolean changeRealLocation;
|
||||
private final Consumer<ReplayResult> onComplete;
|
||||
private final long replayId;
|
||||
private final long startTime;
|
||||
private final Player[] viewers;
|
||||
|
||||
private BukkitTask replayTask;
|
||||
private int currentIndex;
|
||||
private long lastTimestamp;
|
||||
|
||||
private ReplaySession(@NotNull NPC npc, @NotNull MovementRecording recording, double speedMultiplier, boolean changeRealLocation,
|
||||
@Nullable Consumer<ReplayResult> onComplete, long replayId, @NotNull Player... viewers)
|
||||
{
|
||||
this.npc = npc;
|
||||
this.recording = recording;
|
||||
this.speedMultiplier = Math.max(0.1, speedMultiplier); // Minimum 0.1x speed
|
||||
this.changeRealLocation = changeRealLocation;
|
||||
this.onComplete = onComplete;
|
||||
this.replayId = replayId;
|
||||
this.startTime = System.currentTimeMillis();
|
||||
this.currentIndex = 0;
|
||||
this.lastTimestamp = 0;
|
||||
this.viewers = viewers;
|
||||
}
|
||||
|
||||
private void start()
|
||||
{
|
||||
// Teleport NPC to starting position
|
||||
MovementData firstMovement = recording.getFirstMovement();
|
||||
|
||||
if(firstMovement == null)
|
||||
{
|
||||
complete(ReplayResult.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
World world = Bukkit.getWorld(firstMovement.getWorldUUID());
|
||||
if(world == null)
|
||||
{
|
||||
complete(ReplayResult.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Location startLocation = firstMovement.toLocation(world);
|
||||
|
||||
npc.setEnabled(true);
|
||||
for(Player p : viewers)
|
||||
npc.showNPCToPlayer(p);
|
||||
|
||||
Location currentLocation = npc.getLocation();
|
||||
npc.changeRealLocation(startLocation);
|
||||
if(!changeRealLocation)
|
||||
npc.setLocation(currentLocation);
|
||||
|
||||
activeReplays.put(npc.getUUID(), this);
|
||||
|
||||
// Start replay task
|
||||
replayTask = new BukkitRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(currentIndex >= recording.getMovementCount())
|
||||
{
|
||||
complete(ReplayResult.COMPLETED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate timing based on mode - process multiple movements per tick for high speed multipliers
|
||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||
int movementsProcessed = 0;
|
||||
|
||||
while(currentIndex < recording.movements().size() && movementsProcessed < speedMultiplier)
|
||||
{
|
||||
MovementData currentMovement = recording.movements().get(currentIndex);
|
||||
long scaledTime = (long) (currentMovement.getTimestamp() / speedMultiplier);
|
||||
|
||||
if(elapsedTime >= scaledTime)
|
||||
{
|
||||
executeMovement(currentMovement);
|
||||
currentIndex++;
|
||||
movementsProcessed++;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(NpcApi.plugin, 1L, 1L);
|
||||
}
|
||||
|
||||
private void executeMovement(@NotNull MovementData movement)
|
||||
{
|
||||
World world = Bukkit.getWorld(movement.getWorldUUID());
|
||||
if(world == null)
|
||||
{
|
||||
complete(ReplayResult.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Location targetLocation = movement.toLocation(world);
|
||||
|
||||
// Check if this is a teleport (large distance) or smooth movement
|
||||
RotateHeadPacket head = new RotateHeadPacket(npc.entity, (byte) (targetLocation.getYaw() * 256 / 360));
|
||||
if(lastTimestamp > 0)
|
||||
{
|
||||
MovementData previousMovement = recording.movements().get(Math.max(0, currentIndex - 1));
|
||||
double distance = previousMovement.toLocation(world).distance(targetLocation);
|
||||
|
||||
if(distance > 10) // Teleport threshold
|
||||
{
|
||||
|
||||
TeleportEntityPacket teleport1 = new TeleportEntityPacket(npc.entity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(targetLocation.toVector(), new Vector(0, 0, 0), targetLocation.getYaw(),
|
||||
targetLocation.getPitch()), Set.of(), true);
|
||||
npc.sendNpcMovePackets(teleport1, head, viewers);
|
||||
}
|
||||
else
|
||||
{
|
||||
TeleportEntityPacket teleport = new TeleportEntityPacket(npc.entity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(previousMovement.toLocation(world).toVector(),
|
||||
movement.toLocation(world).toVector(), targetLocation.getYaw(), targetLocation.getPitch()), Set.of(), true);
|
||||
|
||||
npc.sendNpcMovePackets(teleport, head, viewers);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// First movement - teleport to position
|
||||
TeleportEntityPacket teleport1 = new TeleportEntityPacket(npc.entity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(targetLocation.toVector(), new Vector(0, 0, 0), targetLocation.getYaw(),
|
||||
targetLocation.getPitch()), Set.of(), true);
|
||||
npc.sendNpcMovePackets(teleport1, head, viewers);
|
||||
}
|
||||
|
||||
if(changeRealLocation)
|
||||
npc.setLocation(targetLocation);
|
||||
|
||||
lastTimestamp = movement.getTimestamp();
|
||||
}
|
||||
|
||||
private void stop()
|
||||
{
|
||||
if(replayTask != null)
|
||||
{
|
||||
replayTask.cancel();
|
||||
replayTask = null;
|
||||
}
|
||||
complete(ReplayResult.CANCELLED);
|
||||
}
|
||||
|
||||
private void complete(@NotNull ReplayResult result)
|
||||
{
|
||||
if(replayTask != null)
|
||||
{
|
||||
replayTask.cancel();
|
||||
replayTask = null;
|
||||
}
|
||||
|
||||
if(npc != null)
|
||||
activeReplays.remove(npc.getUUID());
|
||||
|
||||
if(onComplete != null)
|
||||
onComplete.accept(result);
|
||||
}
|
||||
|
||||
public @Nullable NPC getNpc()
|
||||
{
|
||||
return npc;
|
||||
}
|
||||
|
||||
public long getReplayId()
|
||||
{
|
||||
return replayId;
|
||||
}
|
||||
|
||||
public int getCurrentIndex()
|
||||
{
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
public int getTotalMovements()
|
||||
{
|
||||
return recording.movements().size();
|
||||
}
|
||||
|
||||
public double getProgress()
|
||||
{
|
||||
return recording.movements().isEmpty() ? 1.0 : (double) currentIndex / recording.movements().size();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,34 +1,30 @@
|
||||
package de.eisi05.npc.api.objects;
|
||||
|
||||
import de.eisi05.npc.api.utils.ApiOnly;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Configuration settings for NPC behavior.
|
||||
* This class allows for customizing various aspects of an NPC, such as interaction timers.
|
||||
* Configuration settings for NPC behavior. This class allows for customizing various aspects of an NPC, such as interaction timers.
|
||||
*/
|
||||
public class NpcConfig
|
||||
{
|
||||
/**
|
||||
* The time in ticks an NPC will look at a player after interaction.
|
||||
* The default value is 5 ticks.
|
||||
* The time in ticks an NPC will look at a player after interaction. The default value is 5 ticks.
|
||||
*/
|
||||
private long lookAtTimer = 5;
|
||||
|
||||
/**
|
||||
* If true, command validation will be skipped.
|
||||
* Useful for allowing proxy commands like BungeeCord.
|
||||
* If true, command validation will be skipped. Useful for allowing proxy commands like BungeeCord.
|
||||
*/
|
||||
private boolean avoidCommandCheck = true;
|
||||
|
||||
/**
|
||||
* If true, debug mode is enabled.
|
||||
* Can be used for logging or diagnostic purposes.
|
||||
* If true, debug mode is enabled. Can be used for logging or diagnostic purposes.
|
||||
*/
|
||||
private boolean debug = false;
|
||||
|
||||
/**
|
||||
* Time allowed for input, measured in seconds.
|
||||
* Default is 60 seconds.
|
||||
* Time allowed for input, measured in seconds. Default is 60 seconds.
|
||||
*/
|
||||
private int inputTime = 60;
|
||||
|
||||
@@ -38,11 +34,68 @@ public class NpcConfig
|
||||
private boolean autoUpdate = false;
|
||||
|
||||
/**
|
||||
* The time in ticks a placeholder (Name or Skin) will update.
|
||||
* The default value is 1 minute.
|
||||
* The time in ticks a placeholder (Name or Skin) will update. The default value is 1 minute.
|
||||
*/
|
||||
private long placeholderTimer = 20 * 60;
|
||||
|
||||
/**
|
||||
* If enabled, the system will validate that NPC paths have solid blocks beneath both the starting and ending points of the path.
|
||||
* <p>
|
||||
* When true, the pathfinding will verify that there are solid blocks underneath both the start and end positions of any movement path. If either position
|
||||
* lacks a solid block beneath it, the path will be considered invalid.
|
||||
* <p>
|
||||
* Default: true
|
||||
*/
|
||||
private boolean checkValidPath = true;
|
||||
|
||||
/**
|
||||
* If true, the API will automatically manage NPC visibility for players across various events. This includes showing NPCs when players join, change worlds,
|
||||
* or load chunks, and hiding NPCs when players quit.
|
||||
*/
|
||||
@ApiOnly
|
||||
private boolean autoManageVisibility = true;
|
||||
|
||||
/**
|
||||
* If true, walking NPCs automatically manage viewers while a path task is active.
|
||||
* <p>
|
||||
* When enabled, players who join, change worlds, respawn, or load nearby chunks can be added to the active walking task and synced to the NPC's current
|
||||
* walking position.
|
||||
* <p>
|
||||
* This only applies to walking tasks created without an explicit viewer list.
|
||||
* <p>
|
||||
* Default: true
|
||||
*/
|
||||
@ApiOnly
|
||||
private boolean autoManageWalkingViewers = true;
|
||||
|
||||
/**
|
||||
* Maximum distance in blocks at which walking NPC movement is shown to players.
|
||||
* <p>
|
||||
* Players farther than this distance from the NPC's current walking position will not receive walking movement packets. Players in another world are never
|
||||
* shown walking movement.
|
||||
* <p>
|
||||
* Set to {@code -1} to disable the distance check.
|
||||
* <p>
|
||||
* Default: 64 blocks.
|
||||
*/
|
||||
private double walkingViewerDistance = 64.0;
|
||||
|
||||
/**
|
||||
* If true, precise hitbox detection is used for sleeping NPCs.
|
||||
* <p>
|
||||
* When enabled, the {@link de.eisi05.npc.api.listeners.NpcSleepListener} will use accurate ray intersection calculations to detect interactions with
|
||||
* sleeping NPCs. This provides more precise click detection but may be more computationally expensive.
|
||||
* <p>
|
||||
* Default: false
|
||||
*/
|
||||
private boolean preciseSleepingHitbox = false;
|
||||
|
||||
/**
|
||||
* If true, the NPC will asynchronously load unloaded chunks that it is walking into.
|
||||
* Default: false
|
||||
*/
|
||||
private boolean loadChunksOnPath = false;
|
||||
|
||||
/**
|
||||
* Sets the duration an NPC will look at a player after an interaction.
|
||||
*
|
||||
@@ -56,8 +109,7 @@ public class NpcConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to skip command validation.
|
||||
* Useful for allowing BungeeCord or proxy commands.
|
||||
* Sets whether to skip command validation. Useful for allowing BungeeCord or proxy commands.
|
||||
*
|
||||
* @param avoidCommandCheck True to skip command checks, false to validate.
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
@@ -116,6 +168,89 @@ public class NpcConfig
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the plugin should validate if a path is valid before an NPC starts moving. When enabled, the plugin will check if there are solid blocks
|
||||
* beneath both the starting and destination locations of the NPC's path.
|
||||
*
|
||||
* @param checkValidPath True to enable path validation, false to disable it.
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig checkValidPath(boolean checkValidPath)
|
||||
{
|
||||
this.checkValidPath = checkValidPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the API should automatically manage NPC visibility for players.
|
||||
*
|
||||
* @param autoManageVisibility True to enable automatic visibility management, false to disable it.
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig autoManageVisibility(boolean autoManageVisibility)
|
||||
{
|
||||
this.autoManageVisibility = autoManageVisibility;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether walking NPCs should automatically manage viewers during active path tasks.
|
||||
* <p>
|
||||
* This only applies when {@link #autoManageVisibility()} is enabled and the walking task was created without an explicit viewer list.
|
||||
*
|
||||
* @param autoManageWalkingViewers true to automatically manage walking viewers, false otherwise
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig autoManageWalkingViewers(boolean autoManageWalkingViewers)
|
||||
{
|
||||
this.autoManageWalkingViewers = autoManageWalkingViewers;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum distance in blocks at which walking NPC movement is shown to players.
|
||||
* <p>
|
||||
* Players farther than this distance from the NPC's current walking position will not receive walking packets. Players in another world are never shown
|
||||
* walking movement.
|
||||
* <p>
|
||||
* Use {@code -1} to disable the distance check.
|
||||
*
|
||||
* @param walkingViewerDistance the maximum distance in blocks, or -1 to disable
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig walkingViewerDistance(double walkingViewerDistance)
|
||||
{
|
||||
this.walkingViewerDistance = walkingViewerDistance;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether precise hitbox detection is used for sleeping NPCs.
|
||||
* <p>
|
||||
* When enabled, the {@link de.eisi05.npc.api.listeners.NpcSleepListener} will use accurate ray intersection calculations to detect interactions with
|
||||
* sleeping NPCs.
|
||||
*
|
||||
* @param preciseSleepingHitbox true to enable precise hitbox detection, false otherwise
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig preciseSleepingHitbox(boolean preciseSleepingHitbox)
|
||||
{
|
||||
this.preciseSleepingHitbox = preciseSleepingHitbox;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether NPCs should load unloaded chunks on their path.
|
||||
*
|
||||
* @param loadChunksOnPath true to enable chunk loading, false otherwise
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig loadChunksOnPath(boolean loadChunksOnPath)
|
||||
{
|
||||
this.loadChunksOnPath = loadChunksOnPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured duration an NPC will look at a player.
|
||||
*
|
||||
@@ -175,4 +310,64 @@ public class NpcConfig
|
||||
{
|
||||
return placeholderTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether path validation is enabled for NPC movement.
|
||||
*
|
||||
* @return True if path validation is enabled, false otherwise.
|
||||
*/
|
||||
public boolean checkValidPath()
|
||||
{
|
||||
return checkValidPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the API automatically manages NPC visibility for players.
|
||||
*
|
||||
* @return True if automatic visibility management is enabled; false otherwise.
|
||||
*/
|
||||
public boolean autoManageVisibility()
|
||||
{
|
||||
return autoManageVisibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether walking NPCs automatically manage viewers during active path tasks.
|
||||
*
|
||||
* @return true if automatic walking viewer management is enabled; false otherwise
|
||||
*/
|
||||
public boolean autoManageWalkingViewers()
|
||||
{
|
||||
return autoManageWalkingViewers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum distance in blocks at which walking NPC movement is shown to players.
|
||||
*
|
||||
* @return the maximum walking viewer distance in blocks, or -1 if disabled
|
||||
*/
|
||||
public double walkingViewerDistance()
|
||||
{
|
||||
return walkingViewerDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether precise hitbox detection is enabled for sleeping NPCs.
|
||||
*
|
||||
* @return true if precise hitbox detection is enabled, false otherwise
|
||||
*/
|
||||
public boolean preciseSleepingHitbox()
|
||||
{
|
||||
return preciseSleepingHitbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether NPCs should load unloaded chunks on their path.
|
||||
*
|
||||
* @return true if chunks should be loaded, false otherwise
|
||||
*/
|
||||
public boolean loadChunksOnPath()
|
||||
{
|
||||
return loadChunksOnPath;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,38 @@
|
||||
package de.eisi05.npc.api.objects;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.ai.GoalSelector;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedComponent;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Abstract class representing an entity that can hold NPC-related data
|
||||
* and has a concept of unsaved changes.
|
||||
* It implements {@link InventoryHolder} but onlay as placeholder.
|
||||
* Abstract class representing an entity that can hold NPC-related data and has a concept of unsaved changes. It implements {@link InventoryHolder} but onlay as
|
||||
* placeholder.
|
||||
*/
|
||||
public abstract class NpcHolder implements InventoryHolder
|
||||
{
|
||||
protected static final UUID GLOBAL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
|
||||
protected final Map<UUID, Map<NpcOption<?, ?>, Object>> options = new HashMap<>();
|
||||
private final Map<UUID, NamespacedKey> playerKeyCache = new HashMap<>();
|
||||
|
||||
protected NpcName name;
|
||||
|
||||
/**
|
||||
* Flag indicating whether there are unsaved changes to this holder.
|
||||
* Defaults to {@code false}.
|
||||
* Flag indicating whether there are unsaved changes to this holder. Defaults to {@code false}.
|
||||
*/
|
||||
private boolean unsavedChanges = false;
|
||||
private volatile boolean unsavedChanges = false;
|
||||
|
||||
/**
|
||||
* Checks if there are any unsaved changes for this NPC holder.
|
||||
@@ -31,8 +46,7 @@ public abstract class NpcHolder implements InventoryHolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks that there are unsaved changes to this NPC holder.
|
||||
* This should be called whenever a modifiable property of the holder is changed.
|
||||
* Marks that there are unsaved changes to this NPC holder. This should be called whenever a modifiable property of the holder is changed.
|
||||
*/
|
||||
public void markChange()
|
||||
{
|
||||
@@ -40,9 +54,8 @@ public abstract class NpcHolder implements InventoryHolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current state of the NPC holder.
|
||||
* This method is intended to persist any changes. After successful execution,
|
||||
* the {@code unsavedChanges} flag is reset to {@code false}.
|
||||
* Saves the current state of the NPC holder. This method is intended to persist any changes. After successful execution, the {@code unsavedChanges} flag is
|
||||
* reset to {@code false}.
|
||||
*
|
||||
* @throws IOException if an error occurs during the saving process.
|
||||
*/
|
||||
@@ -51,6 +64,290 @@ public abstract class NpcHolder implements InventoryHolder
|
||||
unsavedChanges = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique identifier of this NPC holder.
|
||||
*
|
||||
* @return The UUID of this NPC holder, or {@code null}
|
||||
*/
|
||||
public abstract @Nullable UUID getUUID();
|
||||
|
||||
/**
|
||||
* Gets the name of this NPC holder.
|
||||
*
|
||||
* @return The Name of this NPC holder, or {@code null}
|
||||
*/
|
||||
public abstract @NotNull WrappedComponent getName();
|
||||
|
||||
/**
|
||||
* Sets the name of this NPC holder.
|
||||
*
|
||||
* @param name The name of this NPC holder
|
||||
*/
|
||||
public abstract void setName(@NotNull NpcName name);
|
||||
|
||||
/**
|
||||
* Gets the goal selector for this NPC holder.
|
||||
*
|
||||
* @return The goal selector for this NPC holder, or {@code null}
|
||||
*/
|
||||
protected abstract @Nullable GoalSelector getGoalSelector();
|
||||
|
||||
/**
|
||||
* Sets an option value for this NPC holder with the global UUID.
|
||||
*
|
||||
* @param <T> the type of the option value
|
||||
* @param option the option to set
|
||||
* @param value the value to set for the option, or {@code null} to remove the option
|
||||
* @throws NullPointerException if the option is null
|
||||
*/
|
||||
public <T> void setOption(@NotNull NpcOption<T, ?> option, @Nullable T value)
|
||||
{
|
||||
setOption(option, value, GLOBAL_UUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an option value for this NPC holder with the specified UUID.
|
||||
*
|
||||
* @param <T> the type of the option value
|
||||
* @param option the option to set
|
||||
* @param value the value to set for the option, or {@code null} to remove the option
|
||||
* @param uuid the UUID to associate with this option setting
|
||||
* @throws NullPointerException if either option or uuid is null
|
||||
*/
|
||||
<T> void setOption(@NotNull NpcOption<T, ?> option, @Nullable T value, @NotNull UUID uuid)
|
||||
{
|
||||
Map<NpcOption<?, ?>, Object> playerOptions = options.computeIfAbsent(uuid, k -> new HashMap<>());
|
||||
if(value == null)
|
||||
{
|
||||
playerOptions.remove(option);
|
||||
if(playerOptions.isEmpty() && !uuid.equals(GLOBAL_UUID))
|
||||
options.remove(uuid);
|
||||
else
|
||||
options.put(uuid, playerOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
playerOptions.put(option, value);
|
||||
options.put(uuid, playerOptions);
|
||||
}
|
||||
|
||||
if(NpcApi.config.autoUpdate() && this instanceof NPC npc)
|
||||
{
|
||||
if(option.equals(NpcOption.SKIN) || option.equals(NpcOption.USE_PLAYER_SKIN))
|
||||
{
|
||||
npc.updateSkin(npc.getViewers().stream().map(Bukkit::getPlayer).filter(Objects::nonNull).toArray(Player[]::new));
|
||||
return;
|
||||
}
|
||||
|
||||
if(option.equals(NpcOption.ENTITY))
|
||||
{
|
||||
npc.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
npc.getViewers().forEach(uuid1 ->
|
||||
{
|
||||
Player player = Bukkit.getPlayer(uuid1);
|
||||
if(player == null)
|
||||
return;
|
||||
|
||||
option.getPacket(value, npc, player).ifPresent(packetWrapper -> WrappedServerPlayer.fromPlayer(player).sendPacket(packetWrapper));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an option value for this NPC holder for a specific player.
|
||||
*
|
||||
* @param <T> the type of the option value
|
||||
* @param option the option to set
|
||||
* @param value the value to set for the option, or {@code null} to remove the option
|
||||
* @param player the player to associate with this option setting
|
||||
*/
|
||||
<T> void setOption(@NotNull NpcOption<T, ?> option, @Nullable T value, @NotNull Player player)
|
||||
{
|
||||
player.getPersistentDataContainer().set(getKey(player), PersistentDataType.STRING, player.getUniqueId().toString());
|
||||
setOption(option, value, player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of an option for a specific UUID.
|
||||
*
|
||||
* @param <T> the type of the option value
|
||||
* @param option the option to get
|
||||
* @param uuid the UUID to get the option for
|
||||
* @return the option value, or the default value if not set
|
||||
* @throws NullPointerException if either option or uuid is null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> @Nullable T getOption(@NotNull NpcOption<T, ?> option, @NotNull UUID uuid)
|
||||
{
|
||||
Map<NpcOption<?, ?>, Object> playerOptions = options.get(uuid);
|
||||
|
||||
if(playerOptions != null && playerOptions.containsKey(option))
|
||||
return (T) playerOptions.get(option);
|
||||
|
||||
Map<NpcOption<?, ?>, Object> globalOptions = options.get(GLOBAL_UUID);
|
||||
if(globalOptions != null && globalOptions.containsKey(option))
|
||||
return (T) globalOptions.get(option);
|
||||
|
||||
return option.getDefaultValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of an option using the global UUID.
|
||||
*
|
||||
* @param <T> the type of the option value
|
||||
* @param option the option to get
|
||||
* @return the option value, or the default value if not set
|
||||
* @throws NullPointerException if option is null
|
||||
*/
|
||||
public <T> @Nullable T getOption(@NotNull NpcOption<T, ?> option)
|
||||
{
|
||||
return getOption(option, GLOBAL_UUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of an option for a specific player.
|
||||
*
|
||||
* @param <T> the type of the option value
|
||||
* @param option the option to get
|
||||
* @param player the player to get the option for
|
||||
* @return the option value, or the default value if not set
|
||||
* @throws NullPointerException if either option or player is null
|
||||
*/
|
||||
public <T> @Nullable T getOption(@NotNull NpcOption<T, ?> option, @NotNull Player player)
|
||||
{
|
||||
if(getUUID() == null)
|
||||
return getOption(option);
|
||||
|
||||
return getOption(option, UUID.fromString(player.getPersistentDataContainer().getOrDefault(getKey(player), PersistentDataType.STRING,
|
||||
GLOBAL_UUID.toString())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique {@link NamespacedKey} for storing player-specific NPC data. The key is constructed using the NPC's UUID and the player's UUID to
|
||||
* ensure uniqueness.
|
||||
*
|
||||
* @param player The player for whom to generate the key
|
||||
* @return A new {@link NamespacedKey} in the format: "npcplugin:[npcUuid]-[playerUuid]"
|
||||
* @throws NullPointerException if the player parameter is null
|
||||
* @throws IllegalStateException if the NPC's UUID is null
|
||||
*/
|
||||
protected @NotNull NamespacedKey getKey(@NotNull Player player)
|
||||
{
|
||||
return playerKeyCache.computeIfAbsent(player.getUniqueId(),
|
||||
uuid -> new NamespacedKey(NpcApi.plugin, getUUID() + "-" + uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies multiple options to this NPC holder for a specific UUID.
|
||||
*
|
||||
* @param options the map of options to apply
|
||||
* @param uuid the UUID to associate with these options
|
||||
* @throws NullPointerException if either options or uuid is null
|
||||
*/
|
||||
public void applyOptions(@NotNull Map<NpcOption<?, ?>, Object> options, @NotNull UUID uuid)
|
||||
{
|
||||
Map<NpcOption<?, ?>, Object> playerOptions = this.options.computeIfAbsent(uuid, k -> new HashMap<>());
|
||||
playerOptions.putAll(options);
|
||||
if(NpcApi.config.autoUpdate() && this instanceof NPC npc)
|
||||
npc.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies multiple options to this NPC holder using the global UUID.
|
||||
*
|
||||
* @param options the map of options to apply
|
||||
* @throws NullPointerException if options is null
|
||||
*/
|
||||
public void applyOptions(@NotNull Map<NpcOption<?, ?>, Object> options)
|
||||
{
|
||||
applyOptions(options, GLOBAL_UUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all options for a specific UUID.
|
||||
*
|
||||
* @param uuid the UUID to get options for
|
||||
* @return a map of options for the specified UUID, or an empty map if none exist
|
||||
* @throws NullPointerException if uuid is null
|
||||
*/
|
||||
public @NotNull Map<NpcOption<?, ?>, Object> getOptions(@NotNull UUID uuid)
|
||||
{
|
||||
return options.getOrDefault(uuid, new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all options associated with the global UUID.
|
||||
*
|
||||
* @return a map of global options, or an empty map if none exist
|
||||
*/
|
||||
public @NotNull Map<NpcOption<?, ?>, Object> getGlobalOptions()
|
||||
{
|
||||
return getOptions(GLOBAL_UUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all options for all UUIDs.
|
||||
*
|
||||
* @return a map of UUIDs to their respective option maps
|
||||
*/
|
||||
public @NotNull Map<UUID, Map<NpcOption<?, ?>, Object>> getOptions()
|
||||
{
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a goal to this NPC's goal selector.
|
||||
*
|
||||
* @param goal The goal to add
|
||||
*/
|
||||
public void addGoal(@NotNull Goal goal)
|
||||
{
|
||||
ArrayList<Goal> goals = getOption(NpcOption.GOALS);
|
||||
goals.add(goal);
|
||||
setOption(NpcOption.GOALS, goals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a goal from this NPC. If the goal is currently running, it will be queued for removal and removed once it finishes naturally. If the goal is not
|
||||
* currently running, it will be immediately removed from the NPC's goal list.
|
||||
*
|
||||
* @param goal The goal to remove
|
||||
* @return true if the goal was removed or queued for removal, false if the goal was not found
|
||||
*/
|
||||
public boolean removeGoal(@NotNull Goal goal)
|
||||
{
|
||||
GoalSelector selector = getGoalSelector();
|
||||
if(selector != null && !selector.queueGoalForRemoval(goal))
|
||||
return true;
|
||||
|
||||
ArrayList<Goal> goals = getOption(NpcOption.GOALS);
|
||||
boolean removed = goals.remove(goal);
|
||||
setOption(NpcOption.GOALS, goals);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of goals associated with this NPC.
|
||||
* <p>
|
||||
* This method returns all active goals for this NPC, excluding any goals that are currently queued for removal. Goals that are in the removal queue will
|
||||
* not appear in the returned list even if they are still running.
|
||||
*
|
||||
* @return A list of active goals, or an empty list if no goals are set
|
||||
*/
|
||||
public @NotNull List<Goal> getGoals()
|
||||
{
|
||||
return getOption(NpcOption.GOALS).stream().filter(goal ->
|
||||
{
|
||||
GoalSelector goalSelector = getGoalSelector();
|
||||
if(goalSelector == null)
|
||||
return true;
|
||||
return !goalSelector.isGoalQueuedForRemoval(goal);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedOperationException always, as this inventory is not used.
|
||||
*/
|
||||
|
||||
@@ -6,13 +6,19 @@ import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.enums.NpcVisibility;
|
||||
import de.eisi05.npc.api.enums.SkinParts;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.manager.NpcVisibilityManager;
|
||||
import de.eisi05.npc.api.scheduler.Tasks;
|
||||
import de.eisi05.npc.api.utils.*;
|
||||
import de.eisi05.npc.api.wrapper.enums.ChatFormat;
|
||||
import de.eisi05.npc.api.wrapper.objects.*;
|
||||
import de.eisi05.npc.api.wrapper.packets.*;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Pose;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
@@ -25,12 +31,11 @@ import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Represents a configurable option for an NPC.
|
||||
* Each option has a path, a default value, serialization/deserialization logic,
|
||||
* and a function to generate a network packet for applying the option.
|
||||
* Represents a configurable option for an NPC. Each option has a path, a default value, serialization/deserialization logic, and a function to generate a
|
||||
* network packet for applying the option.
|
||||
*
|
||||
* @param <T> The type of the option's value in its usable form.
|
||||
* @param <S> The type of the option's value in its serialized form.
|
||||
@@ -38,14 +43,14 @@ import java.util.stream.Collectors;
|
||||
public class NpcOption<T, S extends Serializable>
|
||||
{
|
||||
/**
|
||||
* NPC option to determine if the NPC should use the skin of the viewing player.
|
||||
* If true, the NPC's skin will be dynamically set to the skin of the player looking at it.
|
||||
* NPC option to determine if the NPC should use the skin of the viewing player. If true, the NPC's skin will be dynamically set to the skin of the player
|
||||
* looking at it.
|
||||
*/
|
||||
public static final NpcOption<Boolean, Boolean> USE_PLAYER_SKIN = new NpcOption<>("use-player-skin", false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
public static final NpcOption<Boolean, Boolean> USE_PLAYER_SKIN = new NpcOption<>("use-player-skin", () -> false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
(skin, npc, player) ->
|
||||
{
|
||||
if(!skin)
|
||||
if(!skin || !npc.serverPlayer.equals(npc.entity))
|
||||
return null;
|
||||
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
|
||||
@@ -60,16 +65,21 @@ public class NpcOption<T, S extends Serializable>
|
||||
Property npcProperty = npcTextureProperties.hasNext() ? npcTextureProperties.next() : null;
|
||||
|
||||
if((property == null && npcProperty == null) || (property != null && npcProperty != null &&
|
||||
Reflections.getField(property, "value").get().equals(Reflections.getField(npcProperty, "value").get())))
|
||||
Reflections.getField(property, "value")
|
||||
.get()
|
||||
.equals(Reflections.getField(npcProperty, "value").get())))
|
||||
return null;
|
||||
|
||||
UUID newUUID = UUID.randomUUID();
|
||||
GameProfile profile = Reflections.getInstance(GameProfile.class, newUUID, "NPC" + newUUID.toString().substring(0, 13),
|
||||
GameProfile profile = Reflections.getInstance(GameProfile.class, npc.getUUID(), "NPC" + npc.getUUID().toString().substring(0, 13),
|
||||
Reflections.getInstance(PropertyMap.class, Multimaps.forMap(property == null ? Map.of() : Map.of("textures", property)))
|
||||
.orElseThrow()).orElseThrow();
|
||||
|
||||
npc.serverPlayer = WrappedServerPlayer.create(npc.getLocation(), newUUID, profile, npc.getName(player));
|
||||
npc.changeUUID(newUUID);
|
||||
int id = npc.serverPlayer.getId();
|
||||
NpcManager.removeNPC(npc);
|
||||
npc.serverPlayer = WrappedServerPlayer.create(npc.getLocation(), npc.getUUID(), profile, npc.getName(player),
|
||||
npc.getServerPlayer().getNameTag());
|
||||
npc.serverPlayer.setId(id);
|
||||
NpcManager.addNPC(npc);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -85,42 +95,38 @@ public class NpcOption<T, S extends Serializable>
|
||||
}).loadBefore(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9));
|
||||
|
||||
/**
|
||||
* NPC option to set a specific skin using a value and signature.
|
||||
* This is ignored if {@link #USE_PLAYER_SKIN} is true.
|
||||
* NPC option to set a specific skin using a value and signature. This is ignored if {@link #USE_PLAYER_SKIN} is true.
|
||||
*/
|
||||
public static final NpcOption<NpcSkin, SkinData> SKIN = new NpcOption<NpcSkin, SkinData>("skin", null,
|
||||
skin -> skin, skin -> skin instanceof Skin skin1 ? NpcSkin.of(skin1) : (NpcSkin) skin,
|
||||
public static final NpcOption<NpcSkin, SkinData> SKIN = new NpcOption<NpcSkin, SkinData>("skin", () -> null,
|
||||
skin -> skin, skin -> skin, skin -> skin instanceof Skin skin1 ? NpcSkin.of(skin1) : (NpcSkin) skin,
|
||||
(skinData, npc, player) ->
|
||||
{
|
||||
if(npc.getOption(USE_PLAYER_SKIN) || skinData == null)
|
||||
if(npc.getOption(USE_PLAYER_SKIN, player) || skinData == null || !npc.serverPlayer.equals(npc.entity))
|
||||
return null;
|
||||
|
||||
Skin skin = skinData.getSkin(player, npc);
|
||||
|
||||
if(skin == null)
|
||||
return null;
|
||||
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
|
||||
{
|
||||
var npcTextureProperties = ((PropertyMap) Reflections.getField(npc.getServerPlayer().getGameProfile(), "properties")
|
||||
.get()).get("textures").iterator();
|
||||
|
||||
Property npcProperty = npcTextureProperties.hasNext() ? npcTextureProperties.next() : null;
|
||||
|
||||
if((skin == null && npcProperty == null) ||
|
||||
(npcProperty != null && skin.value().equals(Reflections.getField(npcProperty, "value").get())))
|
||||
if(skin != null && npcProperty != null && skin.value().equals(Reflections.getField(npcProperty, "value").get()))
|
||||
return null;
|
||||
|
||||
UUID newUUID = UUID.randomUUID();
|
||||
PropertyMap propertyMap = Reflections.getInstance(PropertyMap.class,
|
||||
Multimaps.forMap(skin == null ? Map.of() : Map.of("textures", new Property("textures", skin.value(),
|
||||
skin.signature())))).orElseThrow();
|
||||
|
||||
GameProfile profile = Reflections.getInstance(GameProfile.class, newUUID, "NPC" + newUUID.toString().substring(0, 13),
|
||||
GameProfile profile = Reflections.getInstance(GameProfile.class, npc.getUUID(), "NPC" + npc.getUUID().toString().substring(0, 13),
|
||||
propertyMap).orElseThrow();
|
||||
|
||||
npc.serverPlayer = WrappedServerPlayer.create(npc.getLocation(), newUUID, profile, npc.getName(player));
|
||||
npc.changeUUID(newUUID);
|
||||
int id = npc.serverPlayer.getId();
|
||||
NpcManager.removeNPC(npc);
|
||||
npc.serverPlayer = WrappedServerPlayer.create(npc.getLocation(), npc.getUUID(), profile, npc.getName(player),
|
||||
npc.getServerPlayer().getNameTag());
|
||||
npc.serverPlayer.setId(id);
|
||||
NpcManager.addNPC(npc);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -129,55 +135,52 @@ public class NpcOption<T, S extends Serializable>
|
||||
if(skin == null)
|
||||
return null;
|
||||
|
||||
npc.getServerPlayer().getGameProfile().getProperties()
|
||||
.put("textures", new Property("textures", skin.value(), skin.signature()));
|
||||
npc.getServerPlayer().getGameProfile().getProperties().put("textures", new Property("textures", skin.value(), skin.signature()));
|
||||
return null;
|
||||
}).loadBefore(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9));
|
||||
|
||||
/**
|
||||
* NPC option to control whether the NPC is shown in the player tab list.
|
||||
* If false, the NPC will be removed from the tab list for the viewing player after a short delay.
|
||||
* NPC option to control whether the NPC is shown in the player tab list. If false, the NPC will be removed from the tab list for the viewing player after a
|
||||
* short delay.
|
||||
*/
|
||||
public static final NpcOption<Boolean, Boolean> SHOW_TAB_LIST = new NpcOption<>("show-tab-list", true,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
public static final NpcOption<Boolean, Boolean> SHOW_TAB_LIST = new NpcOption<>("show-tab-list", () -> true,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
(show, npc, player) ->
|
||||
{
|
||||
if(show)
|
||||
return null;
|
||||
|
||||
new BukkitRunnable()
|
||||
if(!show || !npc.name.isStatic())
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
new BukkitRunnable()
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_3) && player != null)
|
||||
WrappedServerPlayer.fromPlayer(player).sendPacket(
|
||||
new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.REMOVE_PLAYER, npc.getServerPlayer()));
|
||||
else if(player != null)
|
||||
WrappedServerPlayer.fromPlayer(player)
|
||||
.sendPacket(new PlayerInfoRemovePacket(List.of(npc.getUUID())));
|
||||
}
|
||||
}.runTaskLater(NpcApi.plugin, 50);
|
||||
return null;
|
||||
});
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_3) && player != null)
|
||||
WrappedServerPlayer.fromPlayer(player).sendPacket(
|
||||
new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.REMOVE_PLAYER, npc.getServerPlayer()));
|
||||
else if(player != null && npc.getUUID() != null)
|
||||
WrappedServerPlayer.fromPlayer(player).sendPacket(new PlayerInfoRemovePacket(List.of(npc.getUUID())));
|
||||
}
|
||||
}.runTaskLater(NpcApi.plugin, 50);
|
||||
}
|
||||
return new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.ADD_PLAYER, npc.getServerPlayer());
|
||||
}).loadBefore(true);
|
||||
|
||||
/**
|
||||
* NPC option to set the simulated latency (ping) of the NPC in the tab list.
|
||||
*/
|
||||
public static final NpcOption<Integer, Integer> LATENCY = new NpcOption<>("latency", 0,
|
||||
aInteger -> aInteger, aInteger -> aInteger,
|
||||
public static final NpcOption<Integer, Integer> LATENCY = new NpcOption<>("latency", () -> 0,
|
||||
aInteger -> aInteger, aInteger -> aInteger, aInteger -> aInteger,
|
||||
(latency, npc, player) ->
|
||||
{
|
||||
npc.getServerPlayer().setLatency(latency);
|
||||
|
||||
return new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LATENCY, npc.getServerPlayer());
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to control the visibility of the NPC's nametag.
|
||||
*/
|
||||
public static final NpcOption<Boolean, Boolean> HIDE_NAMETAG = new NpcOption<>("hide-nametag", false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
public static final NpcOption<Boolean, Boolean> HIDE_NAMETAG = new NpcOption<>("hide-nametag", () -> false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
(hide, npc, player) ->
|
||||
{
|
||||
WrappedEntity<?> nameTag = npc.getServerPlayer().getNameTag();
|
||||
@@ -197,43 +200,42 @@ public class NpcOption<T, S extends Serializable>
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to set the pose of the NPC (e.g., standing, sleeping, swimming).
|
||||
* For a full list look at {@link Pose}.
|
||||
*/
|
||||
public static final NpcOption<Pose, Pose> POSE = new NpcOption<>("pose", Pose.STANDING,
|
||||
pose -> pose, pose -> pose,
|
||||
(pose, npc, player) ->
|
||||
{
|
||||
de.eisi05.npc.api.wrapper.enums.Pose nmsPose = de.eisi05.npc.api.wrapper.enums.Pose.fromBukkit(pose);
|
||||
|
||||
if(nmsPose == null)
|
||||
throw new RuntimeException("Pose (" + pose.name() + ") not found");
|
||||
|
||||
npc.getServerPlayer().setPose(nmsPose);
|
||||
|
||||
WrappedEntityData data = npc.getServerPlayer().getEntityData();
|
||||
data.set(WrappedEntityData.EntityDataSerializers.ENTITY_POSE.create(6), nmsPose);
|
||||
|
||||
if(pose == Pose.SPIN_ATTACK)
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(8), (byte) 0x04);
|
||||
else
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(8), (byte) 0x01);
|
||||
|
||||
return SetEntityDataPacket.create(npc.getServerPlayer().getId(), data);
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to set the equipment worn by the NPC (armor, items in hand).
|
||||
* The map uses {@link EquipmentSlot} as keys and {@link ItemStack} as values.
|
||||
* NPC option to set the equipment worn by the NPC (armor, items in hand). The map uses {@link EquipmentSlot} as keys and {@link ItemStack} as values.
|
||||
* Serialized form uses item base64 strings.
|
||||
*/
|
||||
public static final NpcOption<Map<EquipmentSlot, ItemStack>, HashMap<EquipmentSlot, String>> EQUIPMENT = new NpcOption<>("equipment", Map.of(),
|
||||
map -> map.entrySet().stream().collect(
|
||||
Collectors.toMap(Map.Entry::getKey,
|
||||
entry -> ItemSerializer.itemStackToBase64(entry.getValue()), (a, b) -> b, HashMap::new)),
|
||||
serializedMap -> serializedMap.entrySet().stream().collect(
|
||||
Collectors.toMap(Map.Entry::getKey,
|
||||
entry -> ItemSerializer.itemStackFromBase64(entry.getValue()), (a, b) -> b, HashMap::new)),
|
||||
public static final NpcOption<Map<EquipmentSlot, ItemStack>, HashMap<EquipmentSlot, String>> EQUIPMENT = new NpcOption<>("equipment", HashMap::new, HashMap::new,
|
||||
map ->
|
||||
{
|
||||
HashMap<EquipmentSlot, String> serializedMap = new HashMap<>();
|
||||
map.forEach((slot, item) ->
|
||||
{
|
||||
if(item == null || item.getType().isAir())
|
||||
return;
|
||||
|
||||
String serialized = ItemSerializer.itemStackToBase64(item);
|
||||
if(serialized == null)
|
||||
return;
|
||||
|
||||
serializedMap.put(slot, serialized);
|
||||
});
|
||||
return serializedMap;
|
||||
},
|
||||
serializedMap ->
|
||||
{
|
||||
HashMap<EquipmentSlot, ItemStack> map = new HashMap<>();
|
||||
serializedMap.forEach((slot, string) ->
|
||||
{
|
||||
if(string == null || string.isEmpty())
|
||||
return;
|
||||
|
||||
ItemStack item = ItemSerializer.itemStackFromBase64(string);
|
||||
if(item == null)
|
||||
return;
|
||||
|
||||
map.put(slot, item);
|
||||
});
|
||||
return map;
|
||||
},
|
||||
(map, npc, player) ->
|
||||
{
|
||||
if(map.isEmpty())
|
||||
@@ -242,18 +244,16 @@ public class NpcOption<T, S extends Serializable>
|
||||
List<Pair<?, ?>> list = new ArrayList<>();
|
||||
|
||||
map.forEach((slot, item) -> list.add(
|
||||
new Pair<>(de.eisi05.npc.api.wrapper.enums.EquipmentSlot.values()[slot.ordinal()].getHandle(),
|
||||
Var.toNmsItemStack(item))));
|
||||
new Pair<>(de.eisi05.npc.api.wrapper.enums.EquipmentSlot.values()[slot.ordinal()].getHandle(), Var.toNmsItemStack(item))));
|
||||
|
||||
return new SetEquipmentPacket(npc.getServerPlayer().getId(), list);
|
||||
return new SetEquipmentPacket(npc.entity.getId(), list);
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to control which parts of the NPC's skin are visible (e.g., hat, jacket).
|
||||
* For a full list look at {@link SkinParts}.
|
||||
* NPC option to control which parts of the NPC's skin are visible (e.g., hat, jacket). For a full list look at {@link SkinParts}.
|
||||
*/
|
||||
public static final NpcOption<SkinParts[], SkinParts[]> SKIN_PARTS = new NpcOption<>("skin-parts", SkinParts.values(),
|
||||
skinParts -> skinParts, skinParts -> skinParts,
|
||||
public static final NpcOption<SkinParts[], SkinParts[]> SKIN_PARTS = new NpcOption<>("skin-parts", SkinParts::values,
|
||||
skinParts -> skinParts, skinParts -> skinParts, skinParts -> skinParts,
|
||||
(skinParts, npc, player) ->
|
||||
{
|
||||
WrappedEntityData data = npc.getServerPlayer().getEntityData();
|
||||
@@ -263,66 +263,271 @@ public class NpcOption<T, S extends Serializable>
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to make the NPC look at the player if they are within a certain distance.
|
||||
* The value is the maximum distance in blocks. A value of 0 or less disables this.
|
||||
* The actual looking logic is handled by {@link Tasks#lookAtTask()}.
|
||||
* NPC option to make the NPC look at the player if they are within a certain distance. The value is the maximum distance in blocks. A value of 0 or less
|
||||
* disables this. The actual looking logic is handled by {@link Tasks#lookAtTask()}.
|
||||
*/
|
||||
public static final NpcOption<Double, Double> LOOK_AT_PLAYER = new NpcOption<>("look-at-player", 0.0,
|
||||
distance -> distance, distance -> distance,
|
||||
public static final NpcOption<Double, Double> LOOK_AT_PLAYER = new NpcOption<>("look-at-player", () -> 0.0,
|
||||
distance -> distance, distance -> distance, distance -> distance,
|
||||
(distance, npc, player) -> null);
|
||||
|
||||
/**
|
||||
* NPC option to make the NPC glow with a specific color.
|
||||
* If null, the glowing effect is removed.
|
||||
* NPC option to control visibility with three states: fully visible, transparent, or invisible.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>When TRANSPARENT is enabled, it affects other team-based options (COLLISION, GLOWING) by forcing them
|
||||
* to use the same transparent team naming convention</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*/
|
||||
public static final NpcOption<ChatColor, ChatColor> GLOWING = new NpcOption<>("glowing", null,
|
||||
color -> color, color -> color,
|
||||
public static final NpcOption<NpcVisibility, NpcVisibility> VISIBILITY = new NpcOption<>("visibility", () -> NpcVisibility.FULLY_VISIBLE,
|
||||
visibility -> visibility, visibility -> visibility, visibility -> visibility, (visibility, npc, player) ->
|
||||
{
|
||||
String teamName = "trans-" + player.getEntityId();
|
||||
WrappedPlayerTeam playerTeam = WrappedPlayerTeam.getPlayersTeam(player);
|
||||
|
||||
boolean modified = WrappedPlayerTeam.exists(player, teamName) || playerTeam.getHandle() != null;
|
||||
WrappedPlayerTeam wrappedPlayerTeam = playerTeam.getHandle() != null ? playerTeam : WrappedPlayerTeam.create(player, teamName);
|
||||
|
||||
if(!wrappedPlayerTeam.canSeeFriendlyInvisible())
|
||||
wrappedPlayerTeam.setCanSeeFriendlyInvisible(true);
|
||||
|
||||
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified);
|
||||
|
||||
WrappedEntityData entityData = npc.entity.getEntityData();
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(0);
|
||||
Byte value = entityData.get(accessor);
|
||||
byte flags = value == null ? 0 : value;
|
||||
|
||||
byte modifier = 0x20;
|
||||
|
||||
List<PacketWrapper> packets = new ArrayList<>(List.of(teamPacket));
|
||||
|
||||
var removeNpc = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, npc.getServerPlayer().getName(), SetPlayerTeamPacket.Action.REMOVE);
|
||||
|
||||
if(wrappedPlayerTeam.getPlayers().remove(npc.getServerPlayer().getName()))
|
||||
packets.add(removeNpc);
|
||||
|
||||
if(visibility == NpcVisibility.FULLY_VISIBLE)
|
||||
{
|
||||
entityData.set(accessor, (byte) (flags & ~modifier));
|
||||
packets.add(SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
return new BundlePacket(packets.toArray(new PacketWrapper[0]));
|
||||
}
|
||||
|
||||
entityData.set(accessor, (byte) (flags | modifier));
|
||||
|
||||
if(visibility == NpcVisibility.INVISIBLE)
|
||||
{
|
||||
packets.add(SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
return new BundlePacket(packets.toArray(new PacketWrapper[0]));
|
||||
}
|
||||
|
||||
wrappedPlayerTeam.getPlayers().add(npc.getServerPlayer().getName());
|
||||
var addNpc = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, npc.getServerPlayer().getName(), SetPlayerTeamPacket.Action.ADD);
|
||||
var addPlayer = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, player.getName(), SetPlayerTeamPacket.Action.ADD);
|
||||
|
||||
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.entity.getId(), entityData), addNpc, addPlayer);
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to make the NPC glow with a specific color. If null, the glowing effect is removed.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>If TRANSPARENT is enabled, this option will change glowing for all npc with TRANSPARENT enabled</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
public static final NpcOption<ChatColor, ChatColor> GLOWING = new NpcOption<>("glowing", () -> null,
|
||||
color -> color, color -> color, color -> color,
|
||||
(color, npc, player) ->
|
||||
{
|
||||
WrappedEntityData entityData = npc.entity.getEntityData();
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(0);
|
||||
Byte value = entityData.get(accessor);
|
||||
byte flags = value == null ? 0 : value;
|
||||
|
||||
byte modifier = 0x40;
|
||||
|
||||
if(color == null)
|
||||
{
|
||||
WrappedEntityData entityData = npc.getServerPlayer().getEntityData();
|
||||
entityData.set(WrappedEntityData.EntityDataSerializers.BYTE.create(0), (byte) 0);
|
||||
return SetEntityDataPacket.create(npc.getServerPlayer().getId(), entityData);
|
||||
entityData.set(accessor, (byte) (flags & ~modifier));
|
||||
return SetEntityDataPacket.create(npc.entity.getId(), entityData);
|
||||
}
|
||||
|
||||
String teamName = npc.getServerPlayer().getName();
|
||||
boolean modified = WrappedPlayerTeam.exists(player, teamName);
|
||||
WrappedPlayerTeam wrappedPlayerTeam = WrappedPlayerTeam.create(player, teamName);
|
||||
var teamPair = getTeam(player, npc);
|
||||
WrappedPlayerTeam team = teamPair.getFirst();
|
||||
|
||||
wrappedPlayerTeam.setColor(ChatFormat.fromChatColor(color));
|
||||
team.setColor(ChatFormat.fromChatColor(color));
|
||||
|
||||
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified);
|
||||
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(team, !teamPair.getSecond());
|
||||
|
||||
WrappedEntityData entityData = npc.getServerPlayer().getEntityData();
|
||||
entityData.set(WrappedEntityData.EntityDataSerializers.BYTE.create(0), (byte) 0x40);
|
||||
entityData.set(accessor, (byte) (flags | modifier));
|
||||
|
||||
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.getServerPlayer().getId(), entityData));
|
||||
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to set the scale (size) of the NPC.
|
||||
* A value of 1.0 is normal size. Requires Minecraft 1.20.6 or newer.
|
||||
* NPC option to control collision behavior between players and NPCs. When enabled, players can push and collide with NPCs. When disabled, players pass
|
||||
* through NPCs without collision.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>If TRANSPARENT is enabled, this option will change collisions for all npc with TRANSPARENT enabled</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*/
|
||||
public static final NpcOption<Double, Double> SCALE = new NpcOption<>("scale", 1.0,
|
||||
scale -> scale, scale -> scale,
|
||||
public static final NpcOption<Boolean, Boolean> COLLISION = new NpcOption<>("collision", () -> true,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean, (collision, npc, player) ->
|
||||
{
|
||||
var teamPair = getTeam(player, npc);
|
||||
WrappedPlayerTeam team = teamPair.getFirst();
|
||||
team.setCollisionRule(collision ? WrappedPlayerTeam.CollisionRule.ALWAYS : WrappedPlayerTeam.CollisionRule.NEVER);
|
||||
return SetPlayerTeamPacket.createAddOrModifyPacket(team, !teamPair.getSecond());
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to set the pose of the NPC (e.g., standing, sleeping, swimming). For a full list look at {@link Pose}.
|
||||
*/
|
||||
public static final NpcOption<Pose, Pose> POSE = new NpcOption<>("pose", () -> Pose.STANDING,
|
||||
pose -> pose, pose -> pose, pose -> pose,
|
||||
(pose, npc, player) ->
|
||||
{
|
||||
de.eisi05.npc.api.wrapper.enums.Pose nmsPose = de.eisi05.npc.api.wrapper.enums.Pose.fromBukkit(pose);
|
||||
|
||||
if(nmsPose == null)
|
||||
throw new RuntimeException("Pose (" + pose.name() + ") not found");
|
||||
|
||||
npc.getServerPlayer().setPose(nmsPose);
|
||||
|
||||
WrappedEntityData data = npc.entity.getEntityData();
|
||||
data.set(WrappedEntityData.EntityDataSerializers.ENTITY_POSE.create(6), nmsPose);
|
||||
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(0);
|
||||
Byte value = data.get(accessor);
|
||||
byte flags = value == null ? 0 : value;
|
||||
|
||||
PacketWrapper packetWrapper = null;
|
||||
if(pose == Pose.FALL_FLYING)
|
||||
{
|
||||
data.set(accessor, (byte) (flags | 0x80));
|
||||
packetWrapper = new MoveEntityPacket.Rot(npc.entity.getId(), (byte) (npc.getLocation().getYaw() * 256 / 360),
|
||||
(byte) 0, npc.getServerPlayer().isOnGround());
|
||||
}
|
||||
else if(pose == Pose.SWIMMING)
|
||||
data.set(accessor, (byte) (flags | 0x10));
|
||||
else
|
||||
data.set(accessor, (byte) (flags & ~(0x80 | 0x10)));
|
||||
|
||||
if(!npc.entity.getBukkitPlayer().getType().name().equals("ITEM_DISPLAY") &&
|
||||
!npc.entity.getBukkitPlayer().getType().name().equals("BLOCK_DISPLAY"))
|
||||
{
|
||||
Byte handValue = data.get(WrappedEntityData.EntityDataSerializers.BYTE.create(8));
|
||||
byte handFlag = handValue == null ? 0 : handValue;
|
||||
|
||||
if(pose == Pose.SPIN_ATTACK)
|
||||
{
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(8), (byte) (handFlag | 0x04));
|
||||
packetWrapper = new MoveEntityPacket.Rot(npc.entity.getId(), (byte) (npc.getLocation().getYaw() * 256 / 360),
|
||||
(byte) -90, npc.getServerPlayer().isOnGround());
|
||||
}
|
||||
else
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(8), (byte) (handFlag & ~0x04));
|
||||
}
|
||||
|
||||
if(pose == Pose.SLEEPING)
|
||||
{
|
||||
new BukkitRunnable()
|
||||
{
|
||||
int counter = 255;
|
||||
final Location startLocation = npc.getLocation().clone();
|
||||
final float startYaw = startLocation.getYaw();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
startLocation.setYaw((startYaw + counter) % 360);
|
||||
npc.updateLocationForPlayer(startLocation, player);
|
||||
if(counter == 360)
|
||||
cancel();
|
||||
|
||||
counter += 35;
|
||||
}
|
||||
}.runTaskTimer(NpcApi.plugin, 10, 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
new BukkitRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
npc.updateLocationForPlayer(npc.getLocation(), player);
|
||||
}
|
||||
}.runTaskLater(NpcApi.plugin, 1);
|
||||
}
|
||||
|
||||
Integer oldId = npc.toDeleteEntities.remove("sit");
|
||||
if(nmsPose == de.eisi05.npc.api.wrapper.enums.Pose.SITTING)
|
||||
{
|
||||
WrappedTextDisplay textDisplay = WrappedTextDisplay.create(npc.getLocation().getWorld());
|
||||
textDisplay.moveTo(npc.getLocation());
|
||||
npc.toDeleteEntities.put("sit", textDisplay.getId());
|
||||
|
||||
PacketWrapper addEntityPacket = textDisplay.getAddEntityPacket();
|
||||
|
||||
WrappedEntityData wrappedEntityData = textDisplay.getEntityData();
|
||||
wrappedEntityData.set(accessor, (byte) (flags | 0x20));
|
||||
SetEntityDataPacket entityDataPacket = SetEntityDataPacket.create(textDisplay.getId(), wrappedEntityData);
|
||||
|
||||
textDisplay.setPassengers(npc.entity);
|
||||
|
||||
SetPassengerPacket passengerPacket = new SetPassengerPacket(textDisplay);
|
||||
RotateHeadPacket rotateHeadPacket = new RotateHeadPacket(npc.entity, (byte) (npc.getLocation().getYaw() * 256 / 360));
|
||||
|
||||
if(oldId == null)
|
||||
return new BundlePacket(addEntityPacket, entityDataPacket, passengerPacket, rotateHeadPacket);
|
||||
|
||||
return new BundlePacket(new RemoveEntityPacket(oldId), addEntityPacket, entityDataPacket, passengerPacket, rotateHeadPacket);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(oldId == null)
|
||||
return packetWrapper == null ? new BundlePacket(SetEntityDataPacket.create(npc.entity.getId(), data)) :
|
||||
new BundlePacket(packetWrapper, SetEntityDataPacket.create(npc.entity.getId(), data));
|
||||
|
||||
return packetWrapper == null ?
|
||||
new BundlePacket(new RemoveEntityPacket(oldId), SetEntityDataPacket.create(npc.entity.getId(), data)) :
|
||||
new BundlePacket(packetWrapper, new RemoveEntityPacket(oldId), SetEntityDataPacket.create(npc.entity.getId(), data));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to set the scale (size) of the NPC. A value of 1.0 is normal size. Requires Minecraft 1.20.6 or newer.
|
||||
*/
|
||||
public static final NpcOption<Double, Double> SCALE = new NpcOption<>("scale", () -> 1.0,
|
||||
scale -> scale, scale -> scale, scale -> scale,
|
||||
(scale, npc, player) ->
|
||||
{
|
||||
if(npc.entity.getBukkitPlayer().getType().name().equals("ITEM_DISPLAY") ||
|
||||
npc.entity.getBukkitPlayer().getType().name().equals("BLOCK_DISPLAY"))
|
||||
return null;
|
||||
|
||||
WrappedAttributeInstance instance = npc.getServerPlayer().getAttribute(WrappedAttributeInstance.Attributes.SCALE_HOLDER);
|
||||
instance.setBaseValue(scale);
|
||||
|
||||
return new UpdateAttributesPacket(npc.getServerPlayer().getId(), instance);
|
||||
return new UpdateAttributesPacket(npc.entity.getId(), instance);
|
||||
}).since(Versions.V1_20_6);
|
||||
|
||||
/**
|
||||
* NPC option to control the position of the NPC in the TAB list.
|
||||
* <p>
|
||||
* Only works on versions older than 1.21.2.
|
||||
* On 1.21.2 and newer, this option has no effect.
|
||||
* Only works on versions older than 1.21.2. On 1.21.2 and newer, this option has no effect.
|
||||
* </p>
|
||||
*/
|
||||
public static final NpcOption<Integer, Integer> LIST_ORDER = new NpcOption<>("list-order", 0,
|
||||
aInt -> aInt, aInt -> aInt,
|
||||
public static final NpcOption<Integer, Integer> LIST_ORDER = new NpcOption<>("list-order", () -> 0,
|
||||
aInt -> aInt, aInt -> aInt, aInt -> aInt,
|
||||
(order, npc, player) ->
|
||||
{
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_2))
|
||||
@@ -334,12 +539,97 @@ public class NpcOption<T, S extends Serializable>
|
||||
}).since(Versions.V1_21_2);
|
||||
|
||||
/**
|
||||
* NPC option to control if the NPC is enabled (visible and interactable).
|
||||
* If false, a "DISABLED" marker may be shown.
|
||||
* This is an internal option, typically not directly set by users but controlled by {@link NPC#setEnabled(boolean)}.
|
||||
* NPC option to change the entity type of the NPC. This allows transforming the NPC into any Minecraft entity type. The default is a PLAYER entity. When
|
||||
* changed, the NPC will be recreated as the new entity type.
|
||||
*
|
||||
* <p>Note: Some entity types may not work as expected, and certain NPC features may be limited
|
||||
* depending on the entity type. The NPC will maintain its position, name, and other properties when changing entity types.</p>
|
||||
*
|
||||
* <p>Example usage to change to a zombie:
|
||||
* <pre>{@code
|
||||
* npc.setOption(NpcOption.ENTITY, new WrappedEntitySnapshot(EntityType.ZOMBIE));
|
||||
* }</pre>
|
||||
*/
|
||||
static final NpcOption<Boolean, Boolean> ENABLED = new NpcOption<>("enabled", false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
public static final NpcOption<WrappedEntitySnapshot, WrappedEntitySnapshot> ENTITY = new NpcOption<>("entity",
|
||||
() -> new WrappedEntitySnapshot(EntityType.PLAYER), wrappedEntitySnapshot -> wrappedEntitySnapshot,
|
||||
wrappedEntitySnapshot -> wrappedEntitySnapshot, wrappedEntitySnapshot -> wrappedEntitySnapshot,
|
||||
(wrappedEntitySnapshot, npc, player) ->
|
||||
{
|
||||
WrappedEntity<?> entity;
|
||||
if(wrappedEntitySnapshot.getType() == EntityType.PLAYER || wrappedEntitySnapshot.getType().name().equals("MANNEQUIN") ||
|
||||
wrappedEntitySnapshot.getType() == EntityType.UNKNOWN)
|
||||
entity = npc.serverPlayer;
|
||||
else if(wrappedEntitySnapshot.getType() != npc.entity.getBukkitPlayer().getType() ||
|
||||
!npc.entity.data.equals(wrappedEntitySnapshot.getData().toString()))
|
||||
{
|
||||
entity = wrappedEntitySnapshot.create(player.getWorld());
|
||||
entity.moveTo(npc.getLocation());
|
||||
npc.toDeleteEntities.put("entity", entity.getId());
|
||||
}
|
||||
else
|
||||
entity = npc.entity;
|
||||
|
||||
npc.entity = entity;
|
||||
NpcManager.addID(npc.entity.getId(), npc);
|
||||
|
||||
List<PacketWrapper> packets = new ArrayList<>();
|
||||
|
||||
packets.add(new RemoveEntityPacket(npc.getServerPlayer().getId()));
|
||||
packets.add(entity.getAddEntityPacket());
|
||||
|
||||
var teamPair = getTeam(player, npc);
|
||||
WrappedPlayerTeam team = teamPair.getFirst();
|
||||
if(team.getNameTagVisibility() != WrappedPlayerTeam.Visibility.NEVER)
|
||||
team.setNameTagVisibility(WrappedPlayerTeam.Visibility.NEVER);
|
||||
|
||||
if(!team.getName().startsWith("trans"))
|
||||
team.getPlayers().add(npc.getServerPlayer().getName());
|
||||
else
|
||||
(WrappedPlayerTeam.create(player, npc.getServerPlayer().getName())).getPlayers().remove(npc.getServerPlayer().getName());
|
||||
|
||||
packets.add(SetPlayerTeamPacket.createAddOrModifyPacket(team, !teamPair.getSecond()));
|
||||
packets.add(SetPlayerTeamPacket.createPlayerPacket(team, npc.getServerPlayer().getName(), SetPlayerTeamPacket.Action.ADD));
|
||||
packets.add(SetPlayerTeamPacket.createPlayerPacket(team, npc.entity.getBukkitPlayer().getUniqueId().toString(),
|
||||
SetPlayerTeamPacket.Action.ADD));
|
||||
|
||||
WrappedEntityData data = entity.getEntityData();
|
||||
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(0),
|
||||
(byte) (Var.nbtToEntityFlags(wrappedEntitySnapshot.getData()) | Var.extractFlagsFromBukkit(entity.getBukkitPlayer())));
|
||||
data.set(WrappedEntityData.EntityDataSerializers.OPTIONAL_CHAT_COMPONENT.create(2), Optional.of(WrappedComponent.create("NPC").getHandle()));
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BOOLEAN.create(3), false);
|
||||
packets.add(SetEntityDataPacket.create(entity.getId(), data));
|
||||
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_4) || !npc.getOption(NpcOption.HIDE_NAMETAG, player))
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21))
|
||||
npc.serverPlayer.getNameTag()
|
||||
.moveTo(npc.getLocation().clone().add(0,
|
||||
(entity.getBoundingBox().getYSize() * npc.getOption(NpcOption.SCALE, player)), 0));
|
||||
|
||||
packets.add(npc.serverPlayer.getNameTag().getAddEntityPacket());
|
||||
|
||||
packets.add(SetEntityDataPacket.create(npc.serverPlayer.getNameTag().getId(), npc.serverPlayer.getNameTag().applyData(
|
||||
Versions.isCurrentVersionSmallerThan(Versions.V1_19_4) || npc.isEnabled() ? npc.name.getName(player) :
|
||||
WrappedComponent.parseFromLegacy(NpcApi.DISABLED_MESSAGE_PROVIDER.apply(player))
|
||||
.append(WrappedComponent.create("\n").append(npc.name.getName(player))))));
|
||||
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_19_4))
|
||||
{
|
||||
entity.setPassengers(npc.serverPlayer.getNameTag());
|
||||
packets.add(new SetPassengerPacket(entity));
|
||||
}
|
||||
}
|
||||
|
||||
return new BundlePacket(packets.toArray(new PacketWrapper[0]));
|
||||
}).loadBefore(true);
|
||||
|
||||
/**
|
||||
* NPC option to control if the NPC is enabled (visible and interactable). If false, a "DISABLED" marker may be shown. This is an internal option, typically
|
||||
* not directly set by users but controlled by {@link NPC#setEnabled(boolean)}.
|
||||
*/
|
||||
static final NpcOption<Boolean, Boolean> ENABLED = new NpcOption<>("enabled", () -> false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
(enabled, npc, player) ->
|
||||
{
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_19_4))
|
||||
@@ -349,8 +639,8 @@ public class NpcOption<T, S extends Serializable>
|
||||
return null;
|
||||
|
||||
WrappedArmorStand armorStand = WrappedArmorStand.create(npc.getLocation().getWorld());
|
||||
armorStand.moveTo(
|
||||
npc.getLocation().clone().add(0, (npc.getServerPlayer().getBoundingBox().getYSize() * npc.getOption(SCALE)), 0));
|
||||
armorStand.moveTo(npc.getLocation().clone()
|
||||
.add(0, (npc.entity.getBoundingBox().getYSize() * npc.getOption(SCALE, player)), 0));
|
||||
|
||||
PacketWrapper addPacket = armorStand.getAddEntityPacket();
|
||||
|
||||
@@ -363,24 +653,62 @@ public class NpcOption<T, S extends Serializable>
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BOOLEAN.create(4), true);
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(15), (byte) 0x10);
|
||||
|
||||
npc.toDeleteEntities.add(armorStand.getId());
|
||||
npc.toDeleteEntities.put("name", armorStand.getId());
|
||||
|
||||
return new BundlePacket(addPacket, SetEntityDataPacket.create(armorStand.getId(), data));
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to control if the NPC is enabled (visible and interactable).
|
||||
* If false, a "DISABLED" marker may be shown.
|
||||
* This is an internal option, typically not directly set by users but controlled by {@link NPC#setEnabled(boolean)}.
|
||||
* NPC option to control if the NPC is enabled (visible and interactable). If false, a "DISABLED" marker may be shown. This is an internal option, typically
|
||||
* not directly set by users but controlled by {@link NPC#setEnabled(boolean)}.
|
||||
*/
|
||||
static final NpcOption<Boolean, Boolean> EDITABLE = new NpcOption<>("editable", false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
static final NpcOption<Boolean, Boolean> EDITABLE = new NpcOption<>("editable", () -> false,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean,
|
||||
(enabled, npc, player) -> null);
|
||||
|
||||
/**
|
||||
* NPC option to store the goal selector for the NPC. This allows saving and restoring the NPC's AI behavior. The serialized form stores the running state,
|
||||
* tick interval, and goal configurations. Note: Goals themselves are not fully serialized - only their class names and any serializable configuration. On
|
||||
* deserialization, goals must be re-instantiated by the plugin.
|
||||
*/
|
||||
static final NpcOption<ArrayList<Goal>, ArrayList<Goal>> GOALS = new NpcOption<>("goals", ArrayList::new,
|
||||
goals -> new ArrayList<>(goals.stream().map(Goal::copy).toList()), goals -> goals,
|
||||
goals -> goals,
|
||||
(data, npc, player) -> null);
|
||||
|
||||
/**
|
||||
* NPC option to store custom data for the NPC. This is an internal option, typically not directly set by users but controlled by
|
||||
* {@link NPC#addCustomData(Serializable, Serializable)}.
|
||||
*/
|
||||
static final NpcOption<HashMap<Serializable, Serializable>, HashMap<Serializable, Serializable>> CUSTOM_DATA = new NpcOption<>("custom-data",
|
||||
HashMap::new, HashMap::new,
|
||||
aHashMap -> aHashMap, aHashMap -> aHashMap,
|
||||
(customData, npc, player) -> null);
|
||||
|
||||
/**
|
||||
* NPC option to manage visibility settings for the NPC. This controls whether the NPC should be shown to all players (including new ones) or only to specific players.
|
||||
*/
|
||||
static final NpcOption<NpcVisibilityManager, NpcVisibilityManager> VISIBILITY_MANAGER = new NpcOption<>("visibility-manager", NpcVisibilityManager::new,
|
||||
npcVisibilityManager ->
|
||||
{
|
||||
NpcVisibilityManager newManager = new NpcVisibilityManager();
|
||||
newManager.setShowToAllPlayers(npcVisibilityManager.shouldShowToAllPlayers());
|
||||
npcVisibilityManager.getSpecificPlayers().forEach(newManager::addSpecificPlayer);
|
||||
return newManager;
|
||||
},visibilityManager -> visibilityManager, visibilityManager ->
|
||||
{
|
||||
if(visibilityManager.getSpecificPlayers().isEmpty() && !visibilityManager.shouldShowToAllPlayers())
|
||||
visibilityManager.setShowToAllPlayers(true);
|
||||
|
||||
return visibilityManager;
|
||||
},
|
||||
(visibilityManager, npc, player) -> null);
|
||||
|
||||
private final String path;
|
||||
private final T defaultValue;
|
||||
private final Supplier<T> defaultValue;
|
||||
private final Function<T, S> serializer;
|
||||
private final Function<S, T> deserializer;
|
||||
private final Function<T, T> copyFunction;
|
||||
private final TriFunction<T, NPC, Player, PacketWrapper> packet;
|
||||
private Versions since = Versions.V1_17;
|
||||
private boolean loadBefore = false;
|
||||
@@ -390,20 +718,32 @@ public class NpcOption<T, S extends Serializable>
|
||||
*
|
||||
* @param path The configuration path string. Must not be null.
|
||||
* @param defaultValue The default value for the option. Can be null.
|
||||
* @param copyFunction The copy function. Must not be null.
|
||||
* @param serializer The serialization function. Must not be null.
|
||||
* @param deserializer The deserialization function. Must not be null.
|
||||
* @param packet The packet generation function. Must not be null.
|
||||
*/
|
||||
private NpcOption(@NotNull String path, @Nullable T defaultValue, @NotNull Function<T, S> serializer, @NotNull Function<S, T> deserializer,
|
||||
@NotNull TriFunction<T, NPC, Player, PacketWrapper> packet)
|
||||
private NpcOption(@NotNull String path, @Nullable Supplier<T> defaultValue, @NotNull Function<T, T> copyFunction,
|
||||
@NotNull Function<T, S> serializer, @NotNull Function<S, T> deserializer, @NotNull TriFunction<T, NPC, Player, PacketWrapper> packet)
|
||||
{
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.copyFunction = copyFunction;
|
||||
this.serializer = serializer;
|
||||
this.deserializer = deserializer;
|
||||
this.packet = packet;
|
||||
}
|
||||
|
||||
private static @NotNull Pair<WrappedPlayerTeam, Boolean> getTeam(@NotNull Player player, @NotNull NPC npc)
|
||||
{
|
||||
boolean isTransparent = npc.getOption(VISIBILITY, player) == NpcVisibility.TRANSPARENT;
|
||||
WrappedPlayerTeam playerTeam = WrappedPlayerTeam.getPlayersTeam(player);
|
||||
String teamName = isTransparent ? "trans-" + player.getEntityId() : npc.getServerPlayer().getName();
|
||||
boolean modified = WrappedPlayerTeam.exists(player, teamName) || (playerTeam.getHandle() != null && isTransparent);
|
||||
WrappedPlayerTeam wrappedPlayerTeam = playerTeam.getHandle() != null && isTransparent ? playerTeam : WrappedPlayerTeam.create(player, teamName);
|
||||
return new Pair<>(wrappedPlayerTeam, modified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all declared {@link NpcOption} constants within this class using reflection.
|
||||
*
|
||||
@@ -420,7 +760,8 @@ public class NpcOption<T, S extends Serializable>
|
||||
try
|
||||
{
|
||||
values[i] = (NpcOption<?, ?>) fields.get(i).get(null);
|
||||
} catch(IllegalAccessException e)
|
||||
}
|
||||
catch(IllegalAccessException e)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -440,8 +781,7 @@ public class NpcOption<T, S extends Serializable>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum Minecraft version required for this option.
|
||||
* Used for options that are only available in newer versions of the game.
|
||||
* Sets the minimum Minecraft version required for this option. Used for options that are only available in newer versions of the game.
|
||||
*
|
||||
* @param since The minimum {@link Versions} required. Must not be null.
|
||||
* @return This {@link NpcOption} instance for method chaining.
|
||||
@@ -452,21 +792,31 @@ public class NpcOption<T, S extends Serializable>
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this option should be loaded before other options.
|
||||
*
|
||||
* @param loadBefore Whether this option should be loaded before other options.
|
||||
* @return This {@link NpcOption} instance for method chaining.
|
||||
*/
|
||||
public @NotNull NpcOption<T, S> loadBefore(boolean loadBefore)
|
||||
{
|
||||
this.loadBefore = loadBefore;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this option should be loaded before other options.
|
||||
*
|
||||
* @return {@code true} if this option should be loaded before other options, {@code false} otherwise.
|
||||
*/
|
||||
public boolean loadBefore()
|
||||
{
|
||||
return loadBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this NPC option is compatible with the current server version.
|
||||
* An option is compatible if the current server version is greater than or equal to
|
||||
* the version specified by {@link #since()}.
|
||||
* Checks if this NPC option is compatible with the current server version. An option is compatible if the current server version is greater than or equal
|
||||
* to the version specified by {@link #since()}.
|
||||
*
|
||||
* @return {@code true} if the option is compatible, {@code false} otherwise.
|
||||
*/
|
||||
@@ -492,7 +842,18 @@ public class NpcOption<T, S extends Serializable>
|
||||
*/
|
||||
public @Nullable T getDefaultValue()
|
||||
{
|
||||
return defaultValue;
|
||||
return defaultValue.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the current value
|
||||
*
|
||||
* @param value The current value which should be copied
|
||||
* @return A copy of the current value
|
||||
*/
|
||||
public @NotNull T copy(@NotNull T value)
|
||||
{
|
||||
return copyFunction.apply(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -510,8 +871,7 @@ public class NpcOption<T, S extends Serializable>
|
||||
*
|
||||
* @param var1 The value to serialize. Can be null.
|
||||
* @return The serialized value. Can be null if the input or serializer result is null.
|
||||
* @throws RuntimeException if a {@link ClassCastException} occurs during serialization,
|
||||
* indicating an incorrect type was passed.
|
||||
* @throws RuntimeException if a {@link ClassCastException} occurs during serialization, indicating an incorrect type was passed.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public @Nullable S serialize(@Nullable Object var1)
|
||||
@@ -519,7 +879,8 @@ public class NpcOption<T, S extends Serializable>
|
||||
try
|
||||
{
|
||||
return serializer.apply((T) var1);
|
||||
} catch(ClassCastException e)
|
||||
}
|
||||
catch(ClassCastException e)
|
||||
{
|
||||
throw new RuntimeException(path + " -> " + var1);
|
||||
}
|
||||
@@ -537,14 +898,13 @@ public class NpcOption<T, S extends Serializable>
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the network packet(s) needed to apply this option's value to an NPC for a specific player.
|
||||
* The method checks for version compatibility before generating the packet.
|
||||
* Generates the network packet(s) needed to apply this option's value to an NPC for a specific player. The method checks for version compatibility before
|
||||
* generating the packet.
|
||||
*
|
||||
* @param object The value of the option to apply. Can be null.
|
||||
* @param npc The {@link NPC} to apply the option to. Must not be null.
|
||||
* @param player The {@link Player} who will receive the update. Must not be null.
|
||||
* @return An {@link Optional} containing the {@link PacketWrapper} if one is generated and the option is compatible,
|
||||
* otherwise an empty Optional.
|
||||
* @return An {@link Optional} containing the {@link PacketWrapper} if one is generated and the option is compatible, otherwise an empty Optional.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NotNull Optional<PacketWrapper> getPacket(@Nullable Object object, @NotNull NPC npc, @NotNull Player player)
|
||||
@@ -554,4 +914,19 @@ public class NpcOption<T, S extends Serializable>
|
||||
|
||||
return Optional.ofNullable(packet.apply((T) object, npc, player));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if(!(obj instanceof NpcOption<?, ?> npcOption))
|
||||
return false;
|
||||
|
||||
return npcOption.getPath().equals(getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return getPath().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
package de.eisi05.npc.api.objects;
|
||||
|
||||
import de.eisi05.npc.api.scheduler.Tasks;
|
||||
import de.eisi05.npc.api.utils.Reflections;
|
||||
import de.eisi05.npc.api.utils.SerializableBiFunction;
|
||||
import de.eisi05.npc.api.utils.TriFunction;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serial;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Represents the skin data used by an NPC.
|
||||
* <p>
|
||||
* An {@code NpcSkin} can either be:
|
||||
* An {@code NpcSkin} can be:
|
||||
* <ul>
|
||||
* <li><b>Static</b> — using a fixed {@link Skin}.</li>
|
||||
* <li><b>Dynamic</b> — using a {@link SerializableBiFunction} that calculates
|
||||
* a {@link Skin} based on a {@link Player} and {@link NPC}.</li>
|
||||
* <li><b>Dynamic</b> — using a {@link TriFunction} that calculates
|
||||
* a {@link Skin} based on a {@link Player}, {@link NPC}, and a placeholder string.</li>
|
||||
* <li><b>Placeholder-based</b> — using PlaceholderAPI to resolve skins from placeholders.</li>
|
||||
* </ul>
|
||||
* This class is serializable, allowing NPCs to persist their skin information.
|
||||
* </p>
|
||||
@@ -25,18 +32,25 @@ public class NpcSkin implements SkinData
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Skin skin;
|
||||
|
||||
@Deprecated(since = "1.4.5")
|
||||
private final SerializableBiFunction<Player, NPC, Skin> skinFunction;
|
||||
private final String placeholder;
|
||||
private TriFunction<Player, NPC, String, Skin> newSkinFunction;
|
||||
|
||||
/**
|
||||
* Creates a dynamic NPC skin that uses a function to determine the skin.
|
||||
*
|
||||
* @param skinFunction the serializable function that generates a skin based on player and NPC.
|
||||
* @param skinFunction the function that generates a skin based on player, NPC, and placeholder string.
|
||||
* @param placeholder the placeholder string to be used for skin resolution.
|
||||
* @param fallback the fallback {@link Skin} to use if the function returns {@code null}.
|
||||
*/
|
||||
private NpcSkin(@NotNull SerializableBiFunction<Player, NPC, Skin> skinFunction, @NotNull Skin fallback)
|
||||
private NpcSkin(@NotNull TriFunction<Player, NPC, String, Skin> skinFunction, @NotNull String placeholder, @Nullable Skin fallback)
|
||||
{
|
||||
this.skin = fallback;
|
||||
this.skinFunction = skinFunction;
|
||||
this.newSkinFunction = skinFunction;
|
||||
this.placeholder = placeholder;
|
||||
this.skinFunction = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +62,8 @@ public class NpcSkin implements SkinData
|
||||
{
|
||||
this.skin = skin;
|
||||
this.skinFunction = null;
|
||||
this.newSkinFunction = null;
|
||||
this.placeholder = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,9 +84,74 @@ public class NpcSkin implements SkinData
|
||||
* @param fallback the fallback skin if the function fails or returns {@code null}.
|
||||
* @return a new dynamic {@code NpcSkin}.
|
||||
*/
|
||||
public static @NotNull NpcSkin of(@NotNull SerializableBiFunction<Player, NPC, Skin> skinFunction, @NotNull Skin fallback)
|
||||
public static @NotNull NpcSkin of(@NotNull TriFunction<Player, NPC, String, Skin> skinFunction, @NotNull String placeholder, @Nullable Skin fallback)
|
||||
{
|
||||
return new NpcSkin(skinFunction, fallback);
|
||||
return new NpcSkin(skinFunction, placeholder, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link NpcSkin} that resolves its skin using PlaceholderAPI.
|
||||
* <p>
|
||||
* This method creates a dynamic skin that evaluates the provided placeholder string for each player to determine the skin. The placeholder should resolve
|
||||
* to either:
|
||||
* <ul>
|
||||
* <li>A valid UUID string (with or without hyphens)</li>
|
||||
* <li>A valid Minecraft username</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>
|
||||
* The skin is resolved asynchronously when needed. If the placeholder cannot be resolved
|
||||
* or the skin cannot be fetched, the fallback skin will be used.
|
||||
* </p>
|
||||
* <p>
|
||||
* Example usage:
|
||||
* <pre>{@code
|
||||
* // Create a skin that uses a player's skin based on their name
|
||||
* NpcSkin skin = NpcSkin.ofPlaceholderAPI("%player_name%", defaultSkin);
|
||||
* }</pre>
|
||||
* </p>
|
||||
*
|
||||
* @param placeholder The PlaceholderAPI placeholder string to resolve (e.g., "%player_name%")
|
||||
* @param fallback The fallback skin to use if the placeholder cannot be resolved or the skin cannot be fetched. Can be {@code null}.
|
||||
* @return A new {@link NpcSkin} instance that resolves its skin using the specified placeholder
|
||||
* @throws IllegalArgumentException if the placeholder is null
|
||||
* @see Skin#fetchSkin(String)
|
||||
* @see Skin#fetchSkin(UUID)
|
||||
*/
|
||||
public static @NotNull NpcSkin ofPlaceholderAPI(@NotNull String placeholder, @Nullable Skin fallback)
|
||||
{
|
||||
return of((player, npc, s) ->
|
||||
{
|
||||
String newPlaceholder = Tasks.placeholderCache.computeIfAbsent(npc.getUUID(), k -> new ConcurrentHashMap<>()).get(player.getUniqueId());
|
||||
if(newPlaceholder == null)
|
||||
newPlaceholder = (String) Reflections.invokeStaticMethod("me.clip.placeholderapi.PlaceholderAPI", "setPlaceholders", player, s).get();
|
||||
|
||||
if(newPlaceholder == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
UUID uuid = UUID.fromString(newPlaceholder);
|
||||
|
||||
if(Skin.isPreLoaded(uuid))
|
||||
return Skin.fetchSkin(uuid).orElse(null);
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
if(Skin.isPreLoaded(newPlaceholder))
|
||||
return Skin.fetchSkin(newPlaceholder).orElse(null);
|
||||
}
|
||||
return null;
|
||||
}, placeholder, fallback);
|
||||
}
|
||||
|
||||
@Serial
|
||||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
|
||||
{
|
||||
in.defaultReadObject();
|
||||
|
||||
if(newSkinFunction == null)
|
||||
newSkinFunction = ((player, npc, s) -> skinFunction != null ? skinFunction.apply(player, npc) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,23 +161,36 @@ public class NpcSkin implements SkinData
|
||||
*/
|
||||
public @Nullable Skin getSkin()
|
||||
{
|
||||
return skin.value() == null || skin.value().isEmpty() ? null : skin;
|
||||
return skin == null || skin.value() == null || skin.value().isEmpty() ? null : skin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the skin for a specific player and NPC.
|
||||
* <p>
|
||||
* If this is a dynamic skin, the result of the {@link SerializableBiFunction} is returned.
|
||||
* Otherwise, the static skin is used.
|
||||
* If this is a dynamic skin, the result of the {@link TriFunction} is returned. The function receives the player, NPC, and placeholder string as
|
||||
* parameters. If the function returns null or this is a static skin, the fallback skin is used.
|
||||
* </p>
|
||||
*
|
||||
* @param player the player for whom to get the skin.
|
||||
* @param npc the NPC whose skin is being retrieved.
|
||||
* @return the resolved {@link Skin}, or {@code null} if unavailable.
|
||||
* @return the resolved {@link Skin}, or the fallback skin if unavailable, or {@code null} if no fallback exists.
|
||||
*/
|
||||
public @Nullable Skin getSkin(@NotNull Player player, @NotNull NPC npc)
|
||||
{
|
||||
return skinFunction != null ? skinFunction.apply(player, npc) : getSkin();
|
||||
Skin skin = null;
|
||||
if(newSkinFunction != null)
|
||||
skin = newSkinFunction.apply(player, npc, placeholder);
|
||||
return skin != null ? skin : getSkin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the placeholder string used for dynamic skin resolution.
|
||||
*
|
||||
* @return the placeholder string, or {@code null} if this is a static skin.
|
||||
*/
|
||||
public @Nullable String getPlaceholder()
|
||||
{
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +200,7 @@ public class NpcSkin implements SkinData
|
||||
*/
|
||||
public boolean isStatic()
|
||||
{
|
||||
return skinFunction == null;
|
||||
return newSkinFunction == null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,12 +210,11 @@ public class NpcSkin implements SkinData
|
||||
*/
|
||||
public @NotNull NpcSkin copy()
|
||||
{
|
||||
return isStatic() ? new NpcSkin(skin) : new NpcSkin(skinFunction, skin);
|
||||
return isStatic() ? new NpcSkin(skin) : new NpcSkin(newSkinFunction, placeholder, skin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this {@link NpcSkin},
|
||||
* showing whether it is static or dynamic and its associated skin.
|
||||
* Returns a string representation of this {@link NpcSkin}, showing whether it is static or dynamic and its associated skin.
|
||||
*
|
||||
* @return a string describing this skin.
|
||||
*/
|
||||
|
||||
@@ -4,17 +4,13 @@ import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.scheduler.Tasks;
|
||||
import de.eisi05.npc.api.utils.Reflections;
|
||||
import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.utils.exceptions.VersionNotFound;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -23,15 +19,21 @@ import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Scanner;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Represents a player's skin, containing its name, value, and signature.
|
||||
* This record is immutable and implements {@link Serializable} for easy persistence.
|
||||
* The skin value and signature are typically obtained from Mojang's session servers
|
||||
* and are used to display the correct player texture.
|
||||
* Represents a player's skin, containing its name, value, and signature. This record is immutable and implements {@link Serializable} for easy persistence. The
|
||||
* skin value and signature are typically obtained from Mojang's session servers and are used to display the correct player texture.
|
||||
*
|
||||
* @param name The name associated with the skin (usually the player's username). Can be {@code null}.
|
||||
* @param value The base64 encoded string representing the skin data (texture URL, model, etc.).
|
||||
@@ -43,16 +45,14 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* A static cache to store fetched skins, mapping UUIDs to Skin objects.
|
||||
* This helps reduce redundant API calls to Mojang's servers.
|
||||
* A static cache to store fetched skins, mapping UUIDs to Skin objects. This helps reduce redundant API calls to Mojang's servers.
|
||||
*/
|
||||
private static final Map<String, Skin> skinCacheName = new HashMap<>();
|
||||
private static final Map<UUID, Skin> skinCacheUUID = new HashMap<>();
|
||||
private static final Map<File, Skin> skinCacheFile = new HashMap<>();
|
||||
private static final Map<String, Skin> skinCacheName = new ConcurrentHashMap<>();
|
||||
private static final Map<UUID, Skin> skinCacheUUID = new ConcurrentHashMap<>();
|
||||
private static final Map<File, Skin> skinCacheFile = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Retrieves the skin data directly from a currently online Bukkit player.
|
||||
* This method uses reflection to access the player's game profile properties.
|
||||
* Retrieves the skin data directly from a currently online Bukkit player. This method uses reflection to access the player's game profile properties.
|
||||
*
|
||||
* @param player The Bukkit player from whom to retrieve the skin. Must not be {@code null}.
|
||||
* @return A {@link Skin} object representing the player's current skin, or {@code null} if no skin properties are found.
|
||||
@@ -118,9 +118,10 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
} catch(IOException e)
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
skinCacheUUID.put(uuid, null);
|
||||
skinCacheUUID.remove(uuid);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -151,9 +152,10 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
|
||||
"$1-$2-$3-$4-$5")));
|
||||
}
|
||||
} catch(IOException e)
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
skinCacheName.put(name, null);
|
||||
skinCacheName.remove(name);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -173,62 +175,65 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
if(skinCacheFile.containsKey(skinFile))
|
||||
return Optional.ofNullable(skinCacheFile.get(skinFile));
|
||||
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(10_000)
|
||||
.setSocketTimeout(10_000)
|
||||
.build();
|
||||
|
||||
String boundary = "----MineSkinBoundary" + System.currentTimeMillis();
|
||||
|
||||
try(CloseableHttpClient httpClient = HttpClients.custom()
|
||||
.setDefaultRequestConfig(requestConfig)
|
||||
.build();
|
||||
FileInputStream fis = new FileInputStream(skinFile))
|
||||
try
|
||||
{
|
||||
HttpPost upload = new HttpPost("https://api.mineskin.org/generate/upload");
|
||||
upload.setHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
||||
String boundary = "----MineSkinBoundary" + System.currentTimeMillis();
|
||||
String CRLF = "\r\n";
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8), true);
|
||||
byte[] fileBytes = Files.readAllBytes(skinFile.toPath());
|
||||
String bodyBuilder = "--" + boundary + CRLF +
|
||||
"Content-Disposition: form-data; name=\"file\"; filename=\"" +
|
||||
skinFile.getName() + "\"" + CRLF +
|
||||
"Content-Type: image/png" + CRLF + CRLF;
|
||||
|
||||
writer.append("--").append(boundary).append("\r\n");
|
||||
writer.append("Content-Disposition: form-data; name=\"file\"; filename=\"")
|
||||
.append(skinFile.getName()).append("\"\r\n");
|
||||
writer.append("Content-Type: image/png\r\n\r\n");
|
||||
writer.flush();
|
||||
String endPart = CRLF + "--" + boundary + "--" + CRLF;
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
while((len = fis.read(buffer)) != -1)
|
||||
baos.write(buffer, 0, len);
|
||||
baos.flush();
|
||||
byte[] requestBody = combine(bodyBuilder.getBytes(), fileBytes, endPart.getBytes());
|
||||
|
||||
writer.append("\r\n--").append(boundary).append("--\r\n");
|
||||
writer.flush();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("https://api.mineskin.org/generate/upload"))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(requestBody))
|
||||
.build();
|
||||
|
||||
upload.setEntity(new ByteArrayEntity(baos.toByteArray()));
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
JsonObject obj = new JsonParser().parse(response.body()).getAsJsonObject();
|
||||
JsonObject texture = obj.getAsJsonObject("data").getAsJsonObject("texture");
|
||||
|
||||
return httpClient.execute(upload, response ->
|
||||
{
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
String json = EntityUtils.toString(responseEntity);
|
||||
String value = texture.get("value").getAsString();
|
||||
String signature = texture.get("signature").getAsString();
|
||||
|
||||
JsonObject obj = new JsonParser().parse(json).getAsJsonObject();
|
||||
JsonObject texture = obj.getAsJsonObject("data").getAsJsonObject("texture");
|
||||
|
||||
String value = texture.get("value").getAsString();
|
||||
String signature = texture.get("signature").getAsString();
|
||||
|
||||
Skin skin = new Skin(null, value, signature);
|
||||
skinCacheFile.put(skinFile, skin);
|
||||
return Optional.of(skin);
|
||||
});
|
||||
} catch(IOException e)
|
||||
Skin skin = new Skin(null, value, signature);
|
||||
skinCacheFile.put(skinFile, skin);
|
||||
return Optional.of(skin);
|
||||
}
|
||||
catch(IOException | InterruptedException e)
|
||||
{
|
||||
if(NpcApi.config.debug())
|
||||
e.printStackTrace();
|
||||
|
||||
skinCacheFile.remove(skinFile);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] combine(byte[]... arrays) throws IOException
|
||||
{
|
||||
int length = 0;
|
||||
for(byte[] arr : arrays)
|
||||
length += arr.length;
|
||||
byte[] result = new byte[length];
|
||||
int pos = 0;
|
||||
for(byte[] arr : arrays)
|
||||
{
|
||||
System.arraycopy(arr, 0, result, pos, arr.length);
|
||||
pos += arr.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously fetches a skin by the player's UUID.
|
||||
*
|
||||
@@ -237,7 +242,10 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
*/
|
||||
public static CompletableFuture<Optional<Skin>> fetchSkinAsync(@NotNull UUID uuid)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() -> fetchSkin(uuid));
|
||||
CompletableFuture<Optional<Skin>> future = CompletableFuture.supplyAsync(() -> fetchSkin(uuid),
|
||||
runnable -> Bukkit.getScheduler().runTaskAsynchronously(NpcApi.plugin, runnable));
|
||||
Tasks.trackFuture(future);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,7 +256,10 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
*/
|
||||
public static CompletableFuture<Optional<Skin>> fetchSkinAsync(@NotNull String name)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() -> fetchSkin(name));
|
||||
CompletableFuture<Optional<Skin>> future = CompletableFuture.supplyAsync(() -> fetchSkin(name),
|
||||
runnable -> Bukkit.getScheduler().runTaskAsynchronously(NpcApi.plugin, runnable));
|
||||
Tasks.trackFuture(future);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,7 +270,10 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
*/
|
||||
public static CompletableFuture<Optional<Skin>> fetchSkinAsync(@NotNull File skinFile)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() -> fetchSkin(skinFile));
|
||||
CompletableFuture<Optional<Skin>> future = CompletableFuture.supplyAsync(() -> fetchSkin(skinFile),
|
||||
runnable -> Bukkit.getScheduler().runTaskAsynchronously(NpcApi.plugin, runnable));
|
||||
Tasks.trackFuture(future);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,4 +297,14 @@ public record Skin(@Nullable String name, @Nullable String value, @Nullable Stri
|
||||
{
|
||||
return skinCacheUUID.containsKey(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this Skin to a NpcSkin.
|
||||
*
|
||||
* @return the NpcSkin representation of this Skin
|
||||
*/
|
||||
public NpcSkin toNpcSkin()
|
||||
{
|
||||
return NpcSkin.of(this);
|
||||
}
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
package de.eisi05.npc.api.pathfinding;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Implementation of the A* pathfinding algorithm for Bukkit worlds.
|
||||
* Calculates paths between two locations with optional diagonal movement.
|
||||
*/
|
||||
public class AStar
|
||||
{
|
||||
private final int sx;
|
||||
private final int sy;
|
||||
private final int sz;
|
||||
private final int ex;
|
||||
private final int ey;
|
||||
private final int ez;
|
||||
|
||||
private final World w;
|
||||
private final int maxIterations;
|
||||
private final String endUID;
|
||||
private final HashMap<String, Tile> open = new HashMap<>();
|
||||
private final HashMap<String, Tile> closed = new HashMap<>();
|
||||
private final Location start;
|
||||
private final boolean allowDiagonalMovement;
|
||||
private PathingResult result;
|
||||
|
||||
/**
|
||||
* Constructs an A* pathfinder between two locations.
|
||||
*
|
||||
* @param start the starting location
|
||||
* @param end the target location
|
||||
* @param maxIterations maximum number of iterations before aborting
|
||||
* @param allowDiagonalMovement whether diagonal movement is allowed
|
||||
* @throws InvalidPathException if start or end location is not walkable
|
||||
*/
|
||||
public AStar(Location start, Location end, int maxIterations, boolean allowDiagonalMovement) throws InvalidPathException
|
||||
{
|
||||
this.start = start;
|
||||
this.allowDiagonalMovement = allowDiagonalMovement;
|
||||
|
||||
boolean s, e = true;
|
||||
if(!(s = isLocationWalkable(start)) || !(e = isLocationWalkable(end)))
|
||||
throw new InvalidPathException(s, e);
|
||||
this.w = start.getWorld();
|
||||
this.sx = start.getBlockX();
|
||||
this.sy = start.getBlockY();
|
||||
this.sz = start.getBlockZ();
|
||||
this.ex = end.getBlockX();
|
||||
this.ey = end.getBlockY();
|
||||
this.ez = end.getBlockZ();
|
||||
this.maxIterations = maxIterations;
|
||||
short sh = 0;
|
||||
Tile t = new Tile(sh, sh, sh, null);
|
||||
t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
|
||||
this.open.put(t.getUID(), t);
|
||||
processAdjacentTiles(t);
|
||||
this.endUID = String.valueOf(this.ex - this.sx) + (this.ey - this.sy) + (this.ez - this.sz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tile to the open list if not already present.
|
||||
*
|
||||
* @param t the tile to add
|
||||
*/
|
||||
private void addToOpenList(Tile t)
|
||||
{
|
||||
if(!this.open.containsKey(t.getUID()))
|
||||
this.open.put(t.getUID(), t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tile to the closed list if not already present.
|
||||
*
|
||||
* @param t the tile to add
|
||||
*/
|
||||
private void addToClosedList(Tile t)
|
||||
{
|
||||
if(!this.closed.containsKey(t.getUID()))
|
||||
this.closed.put(t.getUID(), t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the starting location of this pathfinding instance.
|
||||
*
|
||||
* @return the start location
|
||||
*/
|
||||
public Location getStart()
|
||||
{
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the end location of this pathfinding instance.
|
||||
*
|
||||
* @return the end location
|
||||
*/
|
||||
public Location getEndLocation()
|
||||
{
|
||||
return new Location(this.w, this.ex, this.ey, this.ez);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the last pathfinding attempt.
|
||||
*
|
||||
* @return pathing result status
|
||||
*/
|
||||
public PathingResult getPathingResult()
|
||||
{
|
||||
return this.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the A* iteration until a path is found or terminated.
|
||||
*
|
||||
* @return list of tiles representing the path, or null if no path exists
|
||||
*/
|
||||
public ArrayList<Tile> iterate()
|
||||
{
|
||||
Tile current = null;
|
||||
int iterations = 0;
|
||||
while(canContinue())
|
||||
{
|
||||
iterations++;
|
||||
if(iterations > this.maxIterations)
|
||||
{
|
||||
this.result = PathingResult.ITERATIONS_EXCEEDED;
|
||||
break;
|
||||
}
|
||||
current = getLowestFTile();
|
||||
processAdjacentTiles(current);
|
||||
}
|
||||
|
||||
if(this.result != PathingResult.SUCCESS)
|
||||
return null;
|
||||
LinkedList<Tile> routeTrace = new LinkedList<>();
|
||||
routeTrace.add(current);
|
||||
Tile parent;
|
||||
|
||||
while((parent = current.getParent()) != null)
|
||||
{
|
||||
routeTrace.add(parent);
|
||||
current = parent;
|
||||
}
|
||||
|
||||
Collections.reverse(routeTrace);
|
||||
return new ArrayList<>(routeTrace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the algorithm can continue.
|
||||
* Stops if the open list is empty or the target has been reached.
|
||||
*
|
||||
* @return true if the search can continue, false otherwise
|
||||
*/
|
||||
private boolean canContinue()
|
||||
{
|
||||
if(this.open.isEmpty())
|
||||
{
|
||||
this.result = PathingResult.NO_PATH;
|
||||
return false;
|
||||
}
|
||||
if(this.closed.containsKey(this.endUID))
|
||||
{
|
||||
this.result = PathingResult.SUCCESS;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes the tile with the lowest f-cost from the open list.
|
||||
*
|
||||
* @return tile with the lowest f-cost
|
||||
*/
|
||||
private Tile getLowestFTile()
|
||||
{
|
||||
double f = 0.0D;
|
||||
Tile drop = null;
|
||||
for(Tile t : this.open.values())
|
||||
{
|
||||
if(f == 0.0D)
|
||||
{
|
||||
t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
|
||||
f = t.getF();
|
||||
drop = t;
|
||||
continue;
|
||||
}
|
||||
t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
|
||||
double posF = t.getF();
|
||||
if(posF < f)
|
||||
{
|
||||
f = posF;
|
||||
drop = t;
|
||||
}
|
||||
}
|
||||
this.open.remove(drop.getUID());
|
||||
addToClosedList(drop);
|
||||
return drop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given tile is already on the closed list.
|
||||
*
|
||||
* @param t the tile to check
|
||||
* @return true if the tile is on the closed list
|
||||
*/
|
||||
private boolean isOnClosedList(Tile t)
|
||||
{
|
||||
return this.closed.containsKey(t.getUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the adjacent tiles around a given tile, adding valid ones to the open list.
|
||||
*
|
||||
* @param current the current tile to expand from
|
||||
*/
|
||||
private void processAdjacentTiles(Tile current)
|
||||
{
|
||||
HashSet<Tile> possible = new HashSet<>(allowDiagonalMovement ? 26 : 14);
|
||||
|
||||
if(allowDiagonalMovement)
|
||||
for(byte x = -1; x <= 1; x = (byte) (x + 1))
|
||||
{
|
||||
for(byte y = -1; y <= 1; y = (byte) (y + 1))
|
||||
{
|
||||
for(byte z = -1; z <= 1; z = (byte) (z + 1))
|
||||
{
|
||||
if(x != 0 || y != 0 || z != 0)
|
||||
{
|
||||
Tile t = new Tile((short) (current.getX() + x), (short) (current.getY() + y), (short) (current.getZ() + z), current);
|
||||
if(!isOnClosedList(t) && isTileWalkable(t))
|
||||
{
|
||||
t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
|
||||
possible.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int[][] directions = {
|
||||
{1, 0, 0}, {-1, 0, 0},
|
||||
{0, 1, 0}, {0, -1, 0},
|
||||
{0, 0, 1}, {0, 0, -1},
|
||||
{0, -1, 1}, {0, -1, -1},
|
||||
{0, 1, 1}, {0, 1, -1},
|
||||
{1, -1, 0}, {-1, -1, 0},
|
||||
{1, 1, 0}, {-1, 1, 0},
|
||||
};
|
||||
|
||||
for(int[] d : directions)
|
||||
{
|
||||
Tile t = new Tile(
|
||||
(short) (current.getX() + d[0]),
|
||||
(short) (current.getY() + d[1]),
|
||||
(short) (current.getZ() + d[2]),
|
||||
current
|
||||
);
|
||||
if(!isOnClosedList(t) && isTileWalkable(t))
|
||||
{
|
||||
t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
|
||||
possible.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Tile t : possible)
|
||||
{
|
||||
Tile openRef;
|
||||
if((openRef = isOnOpenList(t)) == null)
|
||||
{
|
||||
addToOpenList(t);
|
||||
continue;
|
||||
}
|
||||
if(t.getG() < openRef.getG())
|
||||
{
|
||||
openRef.setParent(current);
|
||||
openRef.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a tile if it is already on the open list.
|
||||
*
|
||||
* @param t the tile to check
|
||||
* @return the reference from the open list, or null if not present
|
||||
*/
|
||||
private Tile isOnOpenList(Tile t)
|
||||
{
|
||||
return this.open.getOrDefault(t.getUID(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given tile is walkable (i.e., solid ground with space above).
|
||||
*
|
||||
* @param t the tile to check
|
||||
* @return true if walkable, false otherwise
|
||||
*/
|
||||
private boolean isTileWalkable(Tile t)
|
||||
{
|
||||
Location l = new Location(this.w, (this.sx + t.getX()), (this.sy + t.getY()), (this.sz + t.getZ()));
|
||||
Block b = l.getBlock();
|
||||
return !b.isLiquid() && b.getType().isSolid() && (b.getRelative(0, 1, 0).isPassable() && b.getRelative(0, 2, 0).isPassable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given location is a valid starting or ending point.
|
||||
*
|
||||
* @param l the location to check
|
||||
* @return true if the location is walkable, false otherwise
|
||||
*/
|
||||
private boolean isLocationWalkable(Location l)
|
||||
{
|
||||
Block b = l.getBlock();
|
||||
|
||||
if(!b.isLiquid() && b.getType().isSolid())
|
||||
return ((b.getRelative(0, 1, 0).getType().isAir() || b.getRelative(0, 1, 0).isPassable()) &&
|
||||
(b.getRelative(0, 2, 0).getType().isAir() || b.getRelative(0, 2, 0).isPassable()));
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when path initialization fails due to invalid start or end.
|
||||
*/
|
||||
public static class InvalidPathException extends Exception
|
||||
{
|
||||
private final boolean s;
|
||||
private final boolean e;
|
||||
|
||||
/**
|
||||
* Creates an exception describing invalid start or end tiles.
|
||||
*
|
||||
* @param s whether the start is valid
|
||||
* @param e whether the end is valid
|
||||
*/
|
||||
public InvalidPathException(boolean s, boolean e)
|
||||
{
|
||||
super(getErrorReason(s, e));
|
||||
this.s = s;
|
||||
this.e = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a textual explanation of why the path is invalid.
|
||||
*
|
||||
* @return error reason string
|
||||
*/
|
||||
private static @NotNull String getErrorReason(boolean s, boolean e)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(!s)
|
||||
sb.append("Start Location was air.");
|
||||
if(!e)
|
||||
sb.append("End Location was air.");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the start location was invalid.
|
||||
*
|
||||
* @return true if the start location is not solid
|
||||
*/
|
||||
public boolean isStartNotSolid()
|
||||
{
|
||||
return !this.s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the end location was invalid.
|
||||
*
|
||||
* @return true if the end location is not solid
|
||||
*/
|
||||
public boolean isEndNotSolid()
|
||||
{
|
||||
return !this.e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
package de.eisi05.npc.api.pathfinding;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.Openable;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AStarPathfinder
|
||||
{
|
||||
private static final double[][][] MOVE_COSTS = new double[3][3][3];
|
||||
|
||||
static
|
||||
{
|
||||
for(int x = -1; x <= 1; x++)
|
||||
{
|
||||
for(int y = -1; y <= 1; y++)
|
||||
{
|
||||
for(int z = -1; z <= 1; z++)
|
||||
{
|
||||
if(x == 0 && y == 0 && z == 0)
|
||||
MOVE_COSTS[x + 1][y + 1][z + 1] = 0;
|
||||
else
|
||||
MOVE_COSTS[x + 1][y + 1][z + 1] = (Math.abs(x) + Math.abs(y) + Math.abs(z)) > 1 ? 1.414 : 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final int maxIterations;
|
||||
private final boolean allowDiagonal;
|
||||
private final double entityHeight;
|
||||
private final double entityWidth;
|
||||
private final PriorityQueue<Node> openSet = new PriorityQueue<>();
|
||||
private final Set<Long> openSetIds = new HashSet<>();
|
||||
private final Map<Long, Node> allNodes = new HashMap<>();
|
||||
private World world;
|
||||
|
||||
public AStarPathfinder(int maxIterations, boolean allowDiagonal, double entityHeight, double entityWidth)
|
||||
{
|
||||
this.maxIterations = maxIterations;
|
||||
this.allowDiagonal = allowDiagonal;
|
||||
this.entityHeight = entityHeight;
|
||||
this.entityWidth = entityWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block is valid to stand on.
|
||||
*/
|
||||
public static boolean isSafeFloor(Block block)
|
||||
{
|
||||
if(block == null)
|
||||
return false;
|
||||
|
||||
Material type = block.getType();
|
||||
if(type.isAir() || block.isLiquid())
|
||||
return false;
|
||||
|
||||
return !block.isPassable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a position is valid (not inside a solid block).
|
||||
*
|
||||
* @param world The world to check in
|
||||
* @param tx The x coordinate of the position
|
||||
* @param ty The y coordinate of the position
|
||||
* @param tz The z coordinate of the position
|
||||
* @param entityHeight The height of the entity
|
||||
* @param entityWidth The width of the entity
|
||||
* @return true if the position is valid, false otherwise
|
||||
*/
|
||||
public static boolean isPositionValid(@NotNull World world, double tx, double ty, double tz, double entityHeight, double entityWidth)
|
||||
{
|
||||
double radius = entityWidth / 2.0;
|
||||
double minX = tx - radius;
|
||||
double maxX = tx + radius;
|
||||
double maxY = ty + entityHeight;
|
||||
double minZ = tz - radius;
|
||||
double maxZ = tz + radius;
|
||||
|
||||
BoundingBox entityBox = new BoundingBox(minX, ty, minZ, maxX, maxY, maxZ);
|
||||
|
||||
int minBlockX = (int) Math.floor(minX);
|
||||
int maxBlockX = (int) Math.floor(maxX);
|
||||
int minBlockY = (int) Math.floor(ty);
|
||||
int maxBlockY = (int) Math.floor(maxY);
|
||||
if(maxY > maxBlockY && maxBlockY == minBlockY)
|
||||
maxBlockY++;
|
||||
int minBlockZ = (int) Math.floor(minZ);
|
||||
int maxBlockZ = (int) Math.floor(maxZ);
|
||||
|
||||
for(int x = minBlockX; x <= maxBlockX; x++)
|
||||
{
|
||||
for(int y = minBlockY; y <= maxBlockY; y++)
|
||||
{
|
||||
for(int z = minBlockZ; z <= maxBlockZ; z++)
|
||||
{
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
if(block.getBlockData() instanceof Openable)
|
||||
continue;
|
||||
|
||||
Collection<BoundingBox> blockBoxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(blockBoxes.isEmpty())
|
||||
continue;
|
||||
|
||||
for(BoundingBox blockBox : blockBoxes)
|
||||
{
|
||||
BoundingBox absoluteBox = blockBox.clone().shift(x, y, z);
|
||||
if(entityBox.overlaps(absoluteBox))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public @Nullable List<Location> getPath(@NotNull Location start, @NotNull Location end) throws PathfindingUtils.PathfindingException
|
||||
{
|
||||
if(start.getWorld() == null || end.getWorld() == null)
|
||||
return null;
|
||||
|
||||
if(!start.getWorld().equals(end.getWorld()))
|
||||
return null;
|
||||
|
||||
openSet.clear();
|
||||
openSetIds.clear();
|
||||
allNodes.clear();
|
||||
this.world = start.getWorld();
|
||||
|
||||
int startFloorY = resolveFloorY(start);
|
||||
int endFloorY = resolveFloorY(end);
|
||||
|
||||
Block startFloor = world.getBlockAt(start.getBlockX(), startFloorY, start.getBlockZ());
|
||||
if(NpcApi.config.checkValidPath() && !isSafeFloor(startFloor))
|
||||
throw new PathfindingUtils.PathfindingException("Start not on a valid floor: " + start);
|
||||
|
||||
Block endFloor = world.getBlockAt(end.getBlockX(), endFloorY, end.getBlockZ());
|
||||
if(NpcApi.config.checkValidPath() && !isSafeFloor(endFloor))
|
||||
throw new PathfindingUtils.PathfindingException("End not on a valid floor: " + end);
|
||||
|
||||
Node startNode = new Node(start.getBlockX(), startFloorY, start.getBlockZ(), null);
|
||||
startNode.gCost = 0;
|
||||
startNode.calculateH(end);
|
||||
|
||||
openSet.add(startNode);
|
||||
openSetIds.add(startNode.id);
|
||||
allNodes.put(startNode.id, startNode);
|
||||
|
||||
int iterations = 0;
|
||||
|
||||
while(!openSet.isEmpty())
|
||||
{
|
||||
if(iterations > maxIterations)
|
||||
return null;
|
||||
|
||||
iterations++;
|
||||
|
||||
Node current = openSet.poll();
|
||||
openSetIds.remove(current.id);
|
||||
|
||||
if(distanceSq(current, end) < 1.0)
|
||||
return retracePath(current);
|
||||
|
||||
current.closed = true;
|
||||
|
||||
for(int x = -1; x <= 1; x++)
|
||||
{
|
||||
for(int y = -1; y <= 1; y++)
|
||||
{
|
||||
for(int z = -1; z <= 1; z++)
|
||||
{
|
||||
if(x == 0 && y == 0 && z == 0)
|
||||
continue;
|
||||
|
||||
if(!allowDiagonal && (Math.abs(x) + Math.abs(z) > 1))
|
||||
continue;
|
||||
|
||||
int targetX = current.x + x;
|
||||
int targetY = current.y + y;
|
||||
int targetZ = current.z + z;
|
||||
|
||||
if(!canWalk(current.x, current.y, current.z, targetX, targetY, targetZ))
|
||||
continue;
|
||||
|
||||
long id = Node.hash(targetX, targetY, targetZ);
|
||||
Node neighbor = allNodes.get(id);
|
||||
|
||||
if(neighbor == null)
|
||||
{
|
||||
neighbor = new Node(targetX, targetY, targetZ, id);
|
||||
allNodes.put(id, neighbor);
|
||||
}
|
||||
|
||||
if(neighbor.closed)
|
||||
continue;
|
||||
|
||||
double newGCost = current.gCost + MOVE_COSTS[x + 1][y + 1][z + 1];
|
||||
|
||||
if(newGCost < neighbor.gCost || !openSetIds.contains(id))
|
||||
{
|
||||
neighbor.gCost = newGCost;
|
||||
neighbor.calculateH(end);
|
||||
neighbor.parent = current;
|
||||
|
||||
if(!openSetIds.contains(id))
|
||||
{
|
||||
openSet.add(neighbor);
|
||||
openSetIds.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced physics check. Checks whether we can move from one floor block to another.
|
||||
* <p>
|
||||
* The {@code fy} and {@code ty} values are floor-block Y coordinates. Entity feet and headspace are checked at {@code ty + 1} and {@code ty + 2}.
|
||||
*/
|
||||
private boolean canWalk(int fx, int fy, int fz, int tx, int ty, int tz)
|
||||
{
|
||||
Block floor = world.getBlockAt(tx, ty, tz);
|
||||
if(!isSafeFloor(floor))
|
||||
return false;
|
||||
|
||||
double absoluteFeetY = feetYAt(tx, ty, tz);
|
||||
|
||||
if(!isPositionValid(world, tx + 0.5, absoluteFeetY, tz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
|
||||
if(fx != tx && fz != tz)
|
||||
{
|
||||
double currentFeetY = feetYAt(fx, fy, fz);
|
||||
double checkY = Math.max(currentFeetY, absoluteFeetY);
|
||||
|
||||
if(!isPositionValid(world, fx + 0.5, checkY, tz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
|
||||
if(!isPositionValid(world, tx + 0.5, checkY, fz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
}
|
||||
|
||||
if(fy != ty)
|
||||
{
|
||||
double currentFeetY = feetYAt(fx, fy, fz);
|
||||
double highestFloorY = Math.max(currentFeetY, absoluteFeetY);
|
||||
|
||||
if(!isPositionValid(world, tx + 0.5, highestFloorY, tz + 0.5, entityHeight, entityWidth) ||
|
||||
!isPositionValid(world, fx + 0.5, highestFloorY, fz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the block Y coordinate of the floor beneath a feet-based location. This keeps path nodes aligned with partial collision blocks such as slabs and
|
||||
* stairs.
|
||||
*
|
||||
* @param loc the feet-based location to inspect
|
||||
* @return the Y coordinate of the floor block
|
||||
*/
|
||||
private int resolveFloorY(@NotNull Location loc)
|
||||
{
|
||||
World w = loc.getWorld();
|
||||
if(w == null)
|
||||
return loc.getBlockY() - 1;
|
||||
|
||||
int bx = loc.getBlockX();
|
||||
int bz = loc.getBlockZ();
|
||||
int startY = loc.getBlockY();
|
||||
|
||||
double lx = loc.getX() - bx;
|
||||
double lz = loc.getZ() - bz;
|
||||
|
||||
for(int y = startY + 1; y >= startY - 6; y--)
|
||||
{
|
||||
Block block = w.getBlockAt(bx, y, bz);
|
||||
|
||||
if(block.getBlockData() instanceof Openable)
|
||||
continue;
|
||||
|
||||
if(block.isLiquid())
|
||||
continue;
|
||||
|
||||
if(!block.getType().isSolid() || block.isPassable())
|
||||
continue;
|
||||
|
||||
Collection<BoundingBox> boxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(boxes.isEmpty())
|
||||
return y;
|
||||
|
||||
for(BoundingBox bb : boxes)
|
||||
{
|
||||
if(lx >= bb.getMinX() && lx <= bb.getMaxX() && lz >= bb.getMinZ() && lz <= bb.getMaxZ())
|
||||
return y;
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
return loc.getBlockY() - 1;
|
||||
}
|
||||
|
||||
private @NotNull List<Location> retracePath(@NotNull Node current)
|
||||
{
|
||||
List<Location> path = new ArrayList<>();
|
||||
while(current != null)
|
||||
{
|
||||
double feetY = feetYAt(current.x, current.y, current.z);
|
||||
path.add(new Location(world, current.x + 0.5, feetY, current.z + 0.5));
|
||||
current = current.parent;
|
||||
}
|
||||
Collections.reverse(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private double feetYAt(int x, int floorY, int z)
|
||||
{
|
||||
Block floor = world.getBlockAt(x, floorY, z);
|
||||
return floorY + topSurfaceAt(floor, 0.5, 0.5);
|
||||
}
|
||||
|
||||
private double topSurfaceAt(@NotNull Block block, double lx, double lz)
|
||||
{
|
||||
Collection<BoundingBox> boxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(boxes.isEmpty())
|
||||
return 1.0;
|
||||
|
||||
double bestTop = -1.0;
|
||||
|
||||
for(BoundingBox bb : boxes)
|
||||
{
|
||||
if(lx >= bb.getMinX() && lx <= bb.getMaxX() && lz >= bb.getMinZ() && lz <= bb.getMaxZ())
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
if(bestTop < 0.0)
|
||||
{
|
||||
for(BoundingBox bb : boxes)
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
if(bestTop <= 0.0)
|
||||
return 1.0;
|
||||
|
||||
return bestTop;
|
||||
}
|
||||
|
||||
private double distanceSq(@NotNull Node n, @NotNull Location l)
|
||||
{
|
||||
double dx = (n.x + 0.5) - l.getX();
|
||||
double dy = feetYAt(n.x, n.y, n.z) - l.getY();
|
||||
double dz = (n.z + 0.5) - l.getZ();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static class Node implements Comparable<Node>
|
||||
{
|
||||
final int x, y, z;
|
||||
final long id;
|
||||
|
||||
double gCost = Double.MAX_VALUE;
|
||||
double hCost = 0;
|
||||
Node parent = null;
|
||||
boolean closed = false;
|
||||
|
||||
public Node(int x, int y, int z, Long id)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.id = (id != null) ? id : hash(x, y, z);
|
||||
}
|
||||
|
||||
public static long hash(int x, int y, int z)
|
||||
{
|
||||
return ((long) x & 0x3FFFFFF) | (((long) z & 0x3FFFFFF) << 26) | (((long) y & 0xFFF) << 52);
|
||||
}
|
||||
|
||||
public void calculateH(@NotNull Location end)
|
||||
{
|
||||
double dx = (x + 0.5) - end.getX();
|
||||
double dy = (y + 1.0) - end.getY();
|
||||
double dz = (z + 0.5) - end.getZ();
|
||||
this.hCost = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
public double getFCost()
|
||||
{
|
||||
return gCost + hCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Node other)
|
||||
{
|
||||
return Double.compare(this.getFCost(), other.getFCost());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,12 @@ public class Path implements ConfigurationSerializable
|
||||
waypoints == null ? null : waypoints.stream().map(SerializableLocation::toLocation).toList()).setName(name);
|
||||
}
|
||||
|
||||
private static class SerializableLocation implements Serializable
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public static class SerializableLocation implements Serializable
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.eisi05.npc.api.pathfinding;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -10,17 +12,15 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Utility class for calculating paths between locations using A* pathfinding.
|
||||
* Provides synchronous and asynchronous methods.
|
||||
* Utility class for calculating paths between locations using A* pathfinding. Provides synchronous and asynchronous methods.
|
||||
*/
|
||||
public class PathfindingUtils
|
||||
{
|
||||
/**
|
||||
* Asynchronously calculates a path through a list of waypoints.
|
||||
* <p>
|
||||
* Each segment between consecutive waypoints is calculated in parallel using {@link CompletableFuture}.
|
||||
* The returned future completes with a {@link Path} containing the full path, or completes exceptionally
|
||||
* if an {@link AStar.InvalidPathException} occurs.
|
||||
* Each segment between consecutive waypoints is calculated in parallel using {@link CompletableFuture}. The returned future completes with a {@link Path}
|
||||
* containing the full path, or completes exceptionally if an {@link PathfindingException} occurs.
|
||||
*
|
||||
* @param waypoints the ordered list of locations to traverse
|
||||
* @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment
|
||||
@@ -29,69 +29,129 @@ public class PathfindingUtils
|
||||
* @return a {@link CompletableFuture} that completes with the calculated {@link Path}
|
||||
*/
|
||||
public static @NotNull CompletableFuture<Path> findPathAsync(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return findPath(waypoints, maxIterations, allowDiagonalMovement, progressListener);
|
||||
} catch(AStar.InvalidPathException e)
|
||||
}
|
||||
catch(PathfindingException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}, runnable -> Bukkit.getScheduler().runTaskAsynchronously(NpcApi.plugin, runnable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously calculates a path through a list of waypoints.
|
||||
* <p>
|
||||
* Each segment between consecutive waypoints is calculated in parallel using {@link CompletableFuture}. The returned future completes with a {@link Path}
|
||||
* containing the full path, or completes exceptionally if an {@link PathfindingException} occurs.
|
||||
*
|
||||
* @param waypoints the ordered list of locations to traverse
|
||||
* @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment
|
||||
* @param allowDiagonalMovement whether diagonal movement is allowed
|
||||
* @param entityHeight the height of the entity traversing the path
|
||||
* @param entityWidth the width of the entity traversing the path
|
||||
* @param progressListener a progress listener with the signature (segmentIndex, totalSegments)
|
||||
* @return a {@link CompletableFuture} that completes with the calculated {@link Path}
|
||||
*/
|
||||
public static @NotNull CompletableFuture<Path> findPathAsync(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
double entityHeight, double entityWidth,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return findPath(waypoints, maxIterations, allowDiagonalMovement, entityHeight, entityWidth, progressListener);
|
||||
}
|
||||
catch(PathfindingException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}, runnable -> Bukkit.getScheduler().runTaskAsynchronously(NpcApi.plugin, runnable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously calculates a path through a list of waypoints.
|
||||
* <p>
|
||||
* Each segment between consecutive waypoints is calculated in parallel internally using {@link CompletableFuture},
|
||||
* but this method blocks until all segments are calculated and combined into a single {@link Path}.
|
||||
* Each segment between consecutive waypoints is calculated in parallel internally using {@link CompletableFuture}, but this method blocks until all
|
||||
* segments are calculated and combined into a single {@link Path}.
|
||||
*
|
||||
* @param waypoints the ordered list of locations to traverse
|
||||
* @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment
|
||||
* @param allowDiagonalMovement whether diagonal movement is allowed
|
||||
* @param progressListener a progress listener with the signature (segmentIndex, totalSegments)
|
||||
* @return the calculated {@link Path} containing all intermediate locations
|
||||
* @throws AStar.InvalidPathException if any segment's start or end location is invalid/unwalkable
|
||||
* @throws PathfindingException if any segment's start or end location is invalid/unwalkable
|
||||
*/
|
||||
public static @NotNull Path findPath(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener) throws AStar.InvalidPathException
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener) throws PathfindingException
|
||||
{
|
||||
List<CompletableFuture<List<Location>>> futures = new ArrayList<>();
|
||||
return findPath(waypoints, maxIterations, allowDiagonalMovement, 1.8, 0.6, progressListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously calculates a path through a list of waypoints.
|
||||
* <p>
|
||||
* Each segment between consecutive waypoints is calculated in parallel internally using {@link CompletableFuture}, but this method blocks until all
|
||||
* segments are calculated and combined into a single {@link Path}.
|
||||
*
|
||||
* @param waypoints the ordered list of locations to traverse
|
||||
* @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment
|
||||
* @param allowDiagonalMovement whether diagonal movement is allowed
|
||||
* @param entityHeight the height of the entity traversing the path
|
||||
* @param entityWidth the width of the entity traversing the path
|
||||
* @param progressListener a progress listener with the signature (segmentIndex, totalSegments)
|
||||
* @return the calculated {@link Path} containing all intermediate locations
|
||||
* @throws PathfindingException if any segment's start or end location is invalid/unwalkable
|
||||
*/
|
||||
public static @NotNull Path findPath(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
double entityHeight, double entityWidth, @Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
throws PathfindingException
|
||||
{
|
||||
if(waypoints.size() < 2)
|
||||
throw new IllegalArgumentException("Waypoints list must contain at least 2 locations.");
|
||||
|
||||
List<Location> fullPathPoints = new ArrayList<>();
|
||||
|
||||
AStarPathfinder aStar = new AStarPathfinder(maxIterations, allowDiagonalMovement, entityHeight, entityWidth);
|
||||
for(int i = 0; i < waypoints.size() - 1; i++)
|
||||
{
|
||||
final int idx = i;
|
||||
futures.add(CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
AStar aStar = new AStar(waypoints.get(idx), waypoints.get(idx + 1), maxIterations, allowDiagonalMovement);
|
||||
var segment = aStar.iterate().stream()
|
||||
.map(tile -> tile.getLocation(aStar.getStart()).add(0.5, 1, 0.5))
|
||||
.toList();
|
||||
Location start = waypoints.get(i);
|
||||
Location end = waypoints.get(i + 1);
|
||||
|
||||
if(progressListener != null)
|
||||
progressListener.accept(idx + 1, waypoints.size());
|
||||
List<Location> segment = aStar.getPath(start, end);
|
||||
|
||||
return segment;
|
||||
} catch(AStar.InvalidPathException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}));
|
||||
if(segment == null)
|
||||
throw new PathfindingException("Could not find path between waypoint " + i + " and " + (i + 1));
|
||||
|
||||
if(!fullPathPoints.isEmpty() && !segment.isEmpty())
|
||||
segment.remove(0);
|
||||
|
||||
fullPathPoints.addAll(segment.stream().map(Location::clone).toList());
|
||||
|
||||
if(progressListener != null)
|
||||
progressListener.accept(i + 1, waypoints.size() - 1);
|
||||
}
|
||||
|
||||
Path path = new Path(futures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.flatMap(List::stream)
|
||||
.toList(), waypoints);
|
||||
return new Path(fullPathPoints, waypoints);
|
||||
}
|
||||
|
||||
if(progressListener != null)
|
||||
progressListener.accept(waypoints.size(), waypoints.size());
|
||||
public static class PathfindingException extends Exception
|
||||
{
|
||||
public PathfindingException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
return path;
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
package de.eisi05.npc.api.pathfinding;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
/**
|
||||
* Represents a tile (node) used in the A* pathfinding algorithm.
|
||||
* Each tile stores its relative coordinates, costs, and parent reference.
|
||||
*/
|
||||
public class Tile
|
||||
{
|
||||
private final short x;
|
||||
private final short y;
|
||||
private final short z;
|
||||
|
||||
private final String uid;
|
||||
private double g = -1.0D;
|
||||
private double h = -1.0D;
|
||||
private Tile parent;
|
||||
|
||||
/**
|
||||
* Creates a new tile with given relative coordinates and parent.
|
||||
*
|
||||
* @param x relative x-coordinate
|
||||
* @param y relative y-coordinate
|
||||
* @param z relative z-coordinate
|
||||
* @param parent the parent tile leading to this tile
|
||||
*/
|
||||
public Tile(short x, short y, short z, Tile parent)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.parent = parent;
|
||||
this.uid = String.valueOf(x) + y + z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this tile to an absolute {@link Location} using a start reference.
|
||||
*
|
||||
* @param start the starting location
|
||||
* @return absolute location of this tile
|
||||
*/
|
||||
public Location getLocation(Location start)
|
||||
{
|
||||
return new Location(start.getWorld(), (start.getBlockX() + this.x), (start.getBlockY() + this.y), (start.getBlockZ() + this.z));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent tile of this tile.
|
||||
*
|
||||
* @return the parent tile, or null if none
|
||||
*/
|
||||
public Tile getParent()
|
||||
{
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the parent tile reference.
|
||||
*
|
||||
* @param parent the new parent tile
|
||||
*/
|
||||
public void setParent(Tile parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative x-coordinate.
|
||||
*
|
||||
* @return relative x
|
||||
*/
|
||||
public short getX()
|
||||
{
|
||||
return this.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute x-coordinate relative to a base location.
|
||||
*
|
||||
* @param i base location
|
||||
* @return absolute x
|
||||
*/
|
||||
public int getX(Location i)
|
||||
{
|
||||
return i.getBlockX() + this.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative y-coordinate.
|
||||
*
|
||||
* @return relative y
|
||||
*/
|
||||
public short getY()
|
||||
{
|
||||
return this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute y-coordinate relative to a base location.
|
||||
*
|
||||
* @param i base location
|
||||
* @return absolute y
|
||||
*/
|
||||
public int getY(Location i)
|
||||
{
|
||||
return i.getBlockY() + this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative z-coordinate.
|
||||
*
|
||||
* @return relative z
|
||||
*/
|
||||
public short getZ()
|
||||
{
|
||||
return this.z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute z-coordinate relative to a base location.
|
||||
*
|
||||
* @param i base location
|
||||
* @return absolute z
|
||||
*/
|
||||
public int getZ(Location i)
|
||||
{
|
||||
return i.getBlockZ() + this.z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique identifier string for this tile.
|
||||
*
|
||||
* @return tile UID
|
||||
*/
|
||||
public String getUID()
|
||||
{
|
||||
return this.uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this tile is equal to another based on coordinates.
|
||||
*
|
||||
* @param t the other tile
|
||||
* @return true if coordinates match
|
||||
*/
|
||||
public boolean equals(Tile t)
|
||||
{
|
||||
return (t.getX() == this.x && t.getY() == this.y && t.getZ() == this.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates both G and H values for this tile.
|
||||
*
|
||||
* @param sx start x
|
||||
* @param sy start y
|
||||
* @param sz start z
|
||||
* @param ex end x
|
||||
* @param ey end y
|
||||
* @param ez end z
|
||||
* @param update whether to force recalculation
|
||||
*/
|
||||
public void calculateBoth(int sx, int sy, int sz, int ex, int ey, int ez, boolean update)
|
||||
{
|
||||
calculateG(sx, sy, sz, update);
|
||||
calculateH(sx, sy, sz, ex, ey, ez, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the heuristic (H) cost using Euclidean distance.
|
||||
*
|
||||
* @param sx start x
|
||||
* @param sy start y
|
||||
* @param sz start z
|
||||
* @param ex end x
|
||||
* @param ey end y
|
||||
* @param ez end z
|
||||
* @param update whether to force recalculation
|
||||
*/
|
||||
public void calculateH(int sx, int sy, int sz, int ex, int ey, int ez, boolean update)
|
||||
{
|
||||
if(update || this.h == -1.0D)
|
||||
{
|
||||
int hx = sx + this.x, hy = sy + this.y, hz = sz + this.z;
|
||||
this.h = getEuclideanDistance(hx, hy, hz, ex, ey, ez);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the movement cost (G) from the start to this tile.
|
||||
*
|
||||
* @param sx start x
|
||||
* @param sy start y
|
||||
* @param sz start z
|
||||
* @param update whether to force recalculation
|
||||
*/
|
||||
public void calculateG(int sx, int sy, int sz, boolean update)
|
||||
{
|
||||
if(update || this.g == -1.0D)
|
||||
{
|
||||
Tile currentParent, currentTile = this;
|
||||
int gCost = 0;
|
||||
while((currentParent = currentTile.getParent()) != null)
|
||||
{
|
||||
int dx = currentTile.getX() - currentParent.getX(),
|
||||
dy = currentTile.getY() - currentParent.getY(),
|
||||
dz = currentTile.getZ() - currentParent.getZ();
|
||||
|
||||
dx = abs(dx);
|
||||
dy = abs(dy);
|
||||
dz = abs(dz);
|
||||
|
||||
if(dx == 1 && dy == 1 && dz == 1)
|
||||
gCost = (int) (gCost + 1.7D);
|
||||
|
||||
else if(((dx == 1 || dz == 1) && dy == 1) || ((dx == 1 || dz == 1) && dy == 0))
|
||||
gCost = (int) (gCost + 1.4D);
|
||||
else
|
||||
gCost = (int) (gCost + 1.0D);
|
||||
|
||||
currentTile = currentParent;
|
||||
}
|
||||
this.g = gCost;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the G cost (movement cost from start).
|
||||
*
|
||||
* @return g cost
|
||||
*/
|
||||
public double getG()
|
||||
{
|
||||
return this.g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the H cost (heuristic estimate to end).
|
||||
*
|
||||
* @return h cost
|
||||
*/
|
||||
public double getH()
|
||||
{
|
||||
return this.h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total F cost (G + H).
|
||||
*
|
||||
* @return f cost
|
||||
*/
|
||||
public double getF()
|
||||
{
|
||||
return this.h + this.g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes Euclidean distance between two points.
|
||||
*
|
||||
* @param sx start x
|
||||
* @param sy start y
|
||||
* @param sz start z
|
||||
* @param ex end x
|
||||
* @param ey end y
|
||||
* @param ez end z
|
||||
* @return Euclidean distance
|
||||
*/
|
||||
private double getEuclideanDistance(int sx, int sy, int sz, int ex, int ey, int ez)
|
||||
{
|
||||
double dx = (sx - ex), dy = (sy - ey), dz = (sz - ez);
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of the given integer.
|
||||
*
|
||||
* @param i input value
|
||||
* @return absolute value
|
||||
*/
|
||||
private int abs(int i)
|
||||
{
|
||||
return (i < 0) ? -i : i;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,924 @@
|
||||
package de.eisi05.npc.api.scheduler;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.enums.WalkingResult;
|
||||
import de.eisi05.npc.api.events.NpcStopWalkingEvent;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.pathfinding.AStarPathfinder;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import de.eisi05.npc.api.wrapper.packets.MoveEntityPacket;
|
||||
import de.eisi05.npc.api.wrapper.packets.RotateHeadPacket;
|
||||
import de.eisi05.npc.api.wrapper.packets.TeleportEntityPacket;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.Openable;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A task that handles the movement of an NPC along a calculated path. This class extends BukkitRunnable to handle the movement in a scheduled task, providing
|
||||
* smooth movement, physics, and door interaction capabilities.
|
||||
*/
|
||||
public class PathTask extends BukkitRunnable
|
||||
{
|
||||
private static final double gravity = -0.08;
|
||||
private static final double jumpVelocity = 0.5;
|
||||
private static final double terminalVelocity = -0.5;
|
||||
private static final double stepHeight = 0.55;
|
||||
|
||||
private final NPC npc;
|
||||
private final double entityHeight;
|
||||
private final double entityWidth;
|
||||
private final Path path;
|
||||
private final List<Location> pathPoints;
|
||||
private final Set<UUID> viewerIds = new HashSet<>();
|
||||
private final boolean autoManageWalkingViewers;
|
||||
private final WrappedEntity<?> serverEntity;
|
||||
private final Consumer<WalkingResult> callback;
|
||||
private final boolean withRotation;
|
||||
|
||||
private final double speed;
|
||||
private final boolean updateRealLocation;
|
||||
|
||||
private final Set<Block> openedDoors = new HashSet<>();
|
||||
private final Vector previousMoveDir;
|
||||
|
||||
private boolean finished = false;
|
||||
private int index = 0;
|
||||
private Vector currentPos;
|
||||
private float previousPitch;
|
||||
private float previousYaw;
|
||||
private double verticalVelocity = 0.0;
|
||||
private int viewerRefreshTicks = 0;
|
||||
private boolean isWaitingForChunkLoad = false;
|
||||
|
||||
/**
|
||||
* Private constructor used by the Builder pattern.
|
||||
*
|
||||
* @param builder The builder containing all necessary parameters
|
||||
*/
|
||||
private PathTask(@NotNull Builder builder)
|
||||
{
|
||||
this.npc = builder.npc;
|
||||
double scale = npc.getOption(NpcOption.SCALE);
|
||||
this.entityHeight = npc.entity.getBoundingBox().getYSize() * scale;
|
||||
this.entityWidth = npc.entity.getBoundingBox().getXSize() * scale;
|
||||
this.path = builder.path;
|
||||
this.pathPoints = new ArrayList<>(builder.path.asLocations());
|
||||
if(builder.viewers != null)
|
||||
{
|
||||
for(Player viewer : builder.viewers)
|
||||
{
|
||||
if(viewer != null)
|
||||
this.viewerIds.add(viewer.getUniqueId());
|
||||
}
|
||||
}
|
||||
this.autoManageWalkingViewers = builder.autoManageWalkingViewers;
|
||||
this.callback = builder.callback;
|
||||
this.withRotation = builder.withRotation;
|
||||
|
||||
this.speed = builder.speed;
|
||||
this.updateRealLocation = builder.updateRealLocation;
|
||||
|
||||
this.currentPos = npc.getLocation().toVector();
|
||||
this.previousPitch = npc.getLocation().getPitch();
|
||||
this.previousYaw = npc.getLocation().getYaw();
|
||||
this.previousMoveDir = npc.getLocation().getDirection();
|
||||
this.serverEntity = npc.entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main execution method called by the Bukkit scheduler. Handles the NPC's movement along the path, including physics and door interactions.
|
||||
*/
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
World world = npc.getLocation().getWorld();
|
||||
if(world == null) return;
|
||||
|
||||
int currentChunkX = currentPos.getBlockX() >> 4;
|
||||
int currentChunkZ = currentPos.getBlockZ() >> 4;
|
||||
|
||||
if (!world.isChunkLoaded(currentChunkX, currentChunkZ))
|
||||
{
|
||||
handleUnloadedChunk(world, currentChunkX, currentChunkZ);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < pathPoints.size())
|
||||
{
|
||||
Location next = pathPoints.get(index);
|
||||
int nextChunkX = next.getBlockX() >> 4;
|
||||
int nextChunkZ = next.getBlockZ() >> 4;
|
||||
|
||||
if (!world.isChunkLoaded(nextChunkX, nextChunkZ))
|
||||
{
|
||||
handleUnloadedChunk(world, nextChunkX, nextChunkZ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isWaitingForChunkLoad = false;
|
||||
|
||||
if(autoManageWalkingViewers && viewerRefreshTicks++ >= 10)
|
||||
{
|
||||
viewerRefreshTicks = 0;
|
||||
npc.refreshWalkingViewers();
|
||||
}
|
||||
|
||||
if(index >= pathPoints.size())
|
||||
{
|
||||
if(finishPath())
|
||||
return;
|
||||
}
|
||||
|
||||
Vector target = pathPoints.get(index).toVector();
|
||||
Vector toTarget = target.clone().subtract(currentPos);
|
||||
|
||||
if(hasReachedWaypoint(toTarget))
|
||||
{
|
||||
index++;
|
||||
return;
|
||||
}
|
||||
|
||||
int chunkX = currentPos.getBlockX() >> 4;
|
||||
int chunkZ = currentPos.getBlockZ() >> 4;
|
||||
boolean isChunkLoaded = world.isChunkLoaded(chunkX, chunkZ);
|
||||
|
||||
if(isChunkLoaded)
|
||||
{
|
||||
processDoors();
|
||||
cleanupDoors();
|
||||
|
||||
Vector movement = calculateHorizontalMovement(toTarget, target);
|
||||
|
||||
if(movement.lengthSquared() < 1e-6 && index < pathPoints.size() && currentPos.equals(target))
|
||||
return;
|
||||
|
||||
PhysicsResult physics = applyPhysics(movement, toTarget);
|
||||
|
||||
if(physics.skipHorizontal)
|
||||
{
|
||||
movement.setX(0);
|
||||
movement.setZ(0);
|
||||
}
|
||||
else if(physics.horizontalSlowdown < 1.0)
|
||||
movement.multiply(physics.horizontalSlowdown);
|
||||
|
||||
movement.setY(physics.yChange);
|
||||
|
||||
currentPos.add(movement);
|
||||
|
||||
float yaw, pitch;
|
||||
if(withRotation)
|
||||
{
|
||||
float[] rotation = calculateSmoothRotation();
|
||||
yaw = rotation[0];
|
||||
pitch = rotation[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
yaw = npc.getLocation().getYaw();
|
||||
pitch = npc.getLocation().getPitch();
|
||||
}
|
||||
|
||||
sendMovePackets(movement, yaw, pitch, physics.isGrounded);
|
||||
}
|
||||
else
|
||||
{
|
||||
double distanceToTarget = toTarget.length();
|
||||
double moveDist = Math.min(speed, distanceToTarget);
|
||||
|
||||
if(distanceToTarget > 1e-6)
|
||||
{
|
||||
Vector movement = toTarget.normalize().multiply(moveDist);
|
||||
currentPos.add(movement);
|
||||
}
|
||||
else
|
||||
currentPos = target.clone();
|
||||
|
||||
if(withRotation)
|
||||
{
|
||||
float[] rotation = calculateSmoothRotation();
|
||||
previousYaw = rotation[0];
|
||||
previousPitch = rotation[1];
|
||||
}
|
||||
|
||||
if(updateRealLocation)
|
||||
npc.setLocation(currentPos.toLocation(world));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the logic when an NPC encounters an unloaded chunk.
|
||||
*/
|
||||
private void handleUnloadedChunk(World world, int chunkX, int chunkZ)
|
||||
{
|
||||
if (NpcApi.config.loadChunksOnPath() && !isWaitingForChunkLoad)
|
||||
{
|
||||
isWaitingForChunkLoad = true;
|
||||
world.getChunkAt(chunkX, chunkZ);
|
||||
isWaitingForChunkLoad = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes door interactions along the NPC's path. Opens doors that are in the NPC's path and within interaction range.
|
||||
*/
|
||||
private void processDoors()
|
||||
{
|
||||
World world = npc.getLocation().getWorld();
|
||||
if(world == null)
|
||||
return;
|
||||
|
||||
checkAndOpenDoor(currentPos.toLocation(world).getBlock());
|
||||
checkAndOpenDoor(currentPos.toLocation(world).getBlock().getRelative(BlockFace.UP));
|
||||
|
||||
if(index < pathPoints.size())
|
||||
{
|
||||
Location next = pathPoints.get(index);
|
||||
if(currentPos.distanceSquared(next.toVector()) < 4.0)
|
||||
{
|
||||
checkAndOpenDoor(next.getBlock());
|
||||
checkAndOpenDoor(next.getBlock().getRelative(BlockFace.UP));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block is a door and opens it if it's closed.
|
||||
*
|
||||
* @param block The block to check for door interaction
|
||||
*/
|
||||
private void checkAndOpenDoor(@NotNull Block block)
|
||||
{
|
||||
if(block.getBlockData() instanceof Openable openable)
|
||||
{
|
||||
if(!openable.isOpen())
|
||||
{
|
||||
openable.setOpen(true);
|
||||
block.setBlockData(openable);
|
||||
block.getWorld().playSound(block.getLocation(), org.bukkit.Sound.BLOCK_WOODEN_DOOR_OPEN, 1f, 1f);
|
||||
|
||||
openedDoors.add(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up opened doors that are no longer near the NPC. Closes doors that the NPC has moved away from.
|
||||
*/
|
||||
private void cleanupDoors()
|
||||
{
|
||||
if(openedDoors.isEmpty())
|
||||
return;
|
||||
|
||||
Iterator<Block> iterator = openedDoors.iterator();
|
||||
while(iterator.hasNext())
|
||||
{
|
||||
Block door = iterator.next();
|
||||
if(!(door.getBlockData() instanceof Openable openable))
|
||||
{
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
double distSq = Math.pow(door.getX() + 0.5 - currentPos.getX(), 2) + Math.pow(door.getZ() + 0.5 - currentPos.getZ(), 2);
|
||||
|
||||
if(distSq > 1.69)
|
||||
{
|
||||
if(openable.isOpen())
|
||||
{
|
||||
openable.setOpen(false);
|
||||
door.setBlockData(openable);
|
||||
door.getWorld().playSound(door.getLocation(), org.bukkit.Sound.BLOCK_WOODEN_DOOR_CLOSE, 1f, 1f);
|
||||
}
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces all doors opened by this path task to close. Used when the path is completed or canceled.
|
||||
*/
|
||||
private void forceCloseAllDoors()
|
||||
{
|
||||
for(Block door : openedDoors)
|
||||
{
|
||||
if(door.getBlockData() instanceof Openable openable && openable.isOpen())
|
||||
{
|
||||
openable.setOpen(false);
|
||||
door.setBlockData(openable);
|
||||
door.getWorld().playSound(door.getLocation(), org.bukkit.Sound.BLOCK_WOODEN_DOOR_CLOSE, 1f, 1f);
|
||||
}
|
||||
}
|
||||
openedDoors.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the completion of the path. Performs final cleanup and calls the completion callback.
|
||||
*
|
||||
* @return true if the path was successfully finished, false otherwise
|
||||
*/
|
||||
private boolean finishPath()
|
||||
{
|
||||
Location last = path.getWaypoints().isEmpty() ? null : path.getWaypoints().get(path.getWaypoints().size() - 1);
|
||||
|
||||
if(last != null)
|
||||
{
|
||||
if(currentPos.distanceSquared(last.toVector()) > 0.04)
|
||||
{
|
||||
pathPoints.add(last);
|
||||
return false;
|
||||
}
|
||||
|
||||
smoothEndRotation(last);
|
||||
}
|
||||
|
||||
finished = true;
|
||||
forceCloseAllDoors();
|
||||
|
||||
if(callback != null)
|
||||
callback.accept(WalkingResult.SUCCESS);
|
||||
|
||||
NpcStopWalkingEvent event = new NpcStopWalkingEvent(npc, WalkingResult.SUCCESS, updateRealLocation);
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
|
||||
if(event.changeRealLocation())
|
||||
{
|
||||
Location loc = path.getWaypoints().isEmpty() ? pathPoints.get(pathPoints.size() - 1) : path.getWaypoints().get(path.getWaypoints().size() - 1);
|
||||
npc.changeRealLocation(loc, getViewers());
|
||||
}
|
||||
|
||||
npc.clearWalkingTask(this);
|
||||
cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smoothly rotates the NPC to face the final direction when reaching the end of the path.
|
||||
*
|
||||
* @param loc The target location to face
|
||||
*/
|
||||
private void smoothEndRotation(Location loc)
|
||||
{
|
||||
if(serverEntity == null)
|
||||
return;
|
||||
|
||||
RotateHeadPacket head = new RotateHeadPacket(serverEntity, (byte) (loc.getYaw() * 256 / 360));
|
||||
MoveEntityPacket.Rot body = new MoveEntityPacket.Rot(serverEntity.getId(), (byte) (loc.getYaw() * 256 / 360), (byte) (loc.getPitch() * 256 / 360),
|
||||
true);
|
||||
|
||||
TeleportEntityPacket teleport = new TeleportEntityPacket(serverEntity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(loc.toVector(), new Vector(0, 0, 0), loc.getYaw(), loc.getPitch()), Set.of(), true);
|
||||
|
||||
npc.sendNpcMovePackets(teleport, head, getViewers());
|
||||
npc.sendNpcBodyPackets(body, getViewers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the NPC has reached the current waypoint.
|
||||
*
|
||||
* @param toTarget The vector to the target waypoint
|
||||
* @return true if the waypoint has been reached, false otherwise
|
||||
*/
|
||||
private boolean hasReachedWaypoint(@NotNull Vector toTarget)
|
||||
{
|
||||
double horizontalDistSq = (toTarget.getX() * toTarget.getX()) + (toTarget.getZ() * toTarget.getZ());
|
||||
double verticalDiff = Math.abs(toTarget.getY());
|
||||
return horizontalDistSq <= 0.04 && verticalDiff < 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the horizontal movement vector for the NPC.
|
||||
*
|
||||
* @param toTarget The vector to the target waypoint
|
||||
* @param targetPoint The absolute target point
|
||||
* @return A vector representing the horizontal movement
|
||||
*/
|
||||
private @NotNull Vector calculateHorizontalMovement(@NotNull Vector toTarget, @NotNull Vector targetPoint)
|
||||
{
|
||||
Vector horizontal = new Vector(toTarget.getX(), 0, toTarget.getZ());
|
||||
double distSq = horizontal.lengthSquared();
|
||||
if(distSq < 1e-6)
|
||||
return new Vector(0, 0, 0);
|
||||
|
||||
double dist = Math.sqrt(distSq);
|
||||
double moveDistance = Math.min(speed, dist);
|
||||
Vector moveStep = horizontal.clone().normalize().multiply(moveDistance);
|
||||
|
||||
if(Math.abs(moveDistance - dist) < 1e-6)
|
||||
{
|
||||
this.currentPos = targetPoint.clone();
|
||||
index++;
|
||||
return new Vector(0, 0, 0);
|
||||
}
|
||||
|
||||
return moveStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies physics (gravity, jumping, collision) to the NPC's movement.
|
||||
*
|
||||
* @param movement The current movement vector
|
||||
* @param toTarget The vector to the target waypoint
|
||||
* @return A PhysicsResult containing the vertical movement and ground state
|
||||
*/
|
||||
private @NotNull PhysicsResult applyPhysics(Vector movement, Vector toTarget)
|
||||
{
|
||||
World world = npc.getLocation().getWorld();
|
||||
if(world == null)
|
||||
return new PhysicsResult(0, false, false, 1.0);
|
||||
|
||||
double groundY = getGroundY(world, currentPos);
|
||||
boolean onGround = currentPos.getY() <= groundY + 1e-5;
|
||||
double yChange = 0;
|
||||
|
||||
if(onGround)
|
||||
{
|
||||
if(toTarget.getY() < 0 && Math.abs(toTarget.getY()) <= stepHeight)
|
||||
{
|
||||
Vector testPos = currentPos.clone().add(movement);
|
||||
testPos.setY(currentPos.getY() + toTarget.getY());
|
||||
if(isPositionValid(world, testPos))
|
||||
{
|
||||
yChange = toTarget.getY();
|
||||
verticalVelocity = 0;
|
||||
double slowdown = 0.5;
|
||||
return new PhysicsResult(yChange, true, false, slowdown);
|
||||
}
|
||||
}
|
||||
|
||||
if(toTarget.getY() > 0 && toTarget.getY() <= stepHeight && movement.lengthSquared() > 1e-6)
|
||||
{
|
||||
Vector testPos = currentPos.clone().add(movement);
|
||||
testPos.setY(currentPos.getY() + Math.min(toTarget.getY(), stepHeight));
|
||||
if(!isPositionValid(world, testPos))
|
||||
{
|
||||
yChange = Math.min(toTarget.getY(), stepHeight);
|
||||
verticalVelocity = 0;
|
||||
return new PhysicsResult(yChange, true, true, 1.0);
|
||||
}
|
||||
yChange = Math.min(toTarget.getY(), stepHeight);
|
||||
verticalVelocity = 0;
|
||||
return new PhysicsResult(yChange, true, false, 1.0);
|
||||
}
|
||||
else if(toTarget.getY() > 0.5)
|
||||
{
|
||||
Vector testPos = currentPos.clone().add(movement);
|
||||
testPos.setY(currentPos.getY() + jumpVelocity);
|
||||
if(!isPositionValid(world, testPos))
|
||||
{
|
||||
verticalVelocity = jumpVelocity;
|
||||
return new PhysicsResult(jumpVelocity, false, true, 1.0);
|
||||
}
|
||||
verticalVelocity = jumpVelocity;
|
||||
onGround = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
verticalVelocity = 0;
|
||||
if(Math.abs(currentPos.getY() - groundY) > 1e-6)
|
||||
currentPos.setY(groundY);
|
||||
return new PhysicsResult(0, true, false, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
if(!onGround)
|
||||
{
|
||||
verticalVelocity += gravity;
|
||||
if(verticalVelocity < terminalVelocity)
|
||||
verticalVelocity = terminalVelocity;
|
||||
yChange = verticalVelocity;
|
||||
|
||||
Vector testPos = currentPos.clone().add(movement);
|
||||
testPos.setY(currentPos.getY() + yChange);
|
||||
if(!isPositionValid(world, testPos) && currentPos.getY() + yChange <= groundY + 0.1)
|
||||
{
|
||||
yChange = groundY - currentPos.getY();
|
||||
verticalVelocity = 0;
|
||||
onGround = true;
|
||||
}
|
||||
else if(currentPos.getY() + yChange <= groundY)
|
||||
{
|
||||
yChange = groundY - currentPos.getY();
|
||||
verticalVelocity = 0;
|
||||
onGround = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new PhysicsResult(yChange, onGround, false, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a position is valid (not inside a solid block).
|
||||
*
|
||||
* @param world The world to check in
|
||||
* @param pos The position to check
|
||||
* @return true if the position is valid, false otherwise
|
||||
*/
|
||||
private boolean isPositionValid(@NotNull World world, @NotNull Vector pos)
|
||||
{
|
||||
return AStarPathfinder.isPositionValid(world, pos.getX(), pos.getY(), pos.getZ(), entityHeight, entityWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the feet Y-coordinate of the ground at a given position.
|
||||
*
|
||||
* @param world The world to check in
|
||||
* @param pos The position to check
|
||||
* @return The Y-coordinate where the NPC's feet should be
|
||||
*/
|
||||
private double getGroundY(@NotNull World world, @NotNull Vector pos)
|
||||
{
|
||||
int bx = pos.getBlockX();
|
||||
int bz = pos.getBlockZ();
|
||||
int startY = pos.getBlockY();
|
||||
|
||||
for(int y = startY; y >= startY - 4; y--)
|
||||
{
|
||||
Block block = world.getBlockAt(bx, y, bz);
|
||||
|
||||
if(block.getBlockData() instanceof Openable || block.isLiquid())
|
||||
continue;
|
||||
|
||||
Collection<BoundingBox> boxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(boxes.isEmpty())
|
||||
continue;
|
||||
|
||||
double lx = pos.getX() - bx;
|
||||
double lz = pos.getZ() - bz;
|
||||
|
||||
double bestTop = -1.0;
|
||||
|
||||
for(BoundingBox bb : boxes)
|
||||
{
|
||||
if(lx >= bb.getMinX() && lx <= bb.getMaxX() && lz >= bb.getMinZ() && lz <= bb.getMaxZ())
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
if(bestTop < 0.0)
|
||||
{
|
||||
for(BoundingBox bb : boxes)
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
return y + bestTop;
|
||||
}
|
||||
|
||||
return world.getHighestBlockYAt(bx, bz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates smooth rotation for the NPC's head and body.
|
||||
*
|
||||
* @return An array containing [yaw, pitch] for the NPC's rotation
|
||||
*/
|
||||
private float @NotNull [] calculateSmoothRotation()
|
||||
{
|
||||
Vector lookDir;
|
||||
if(index + 1 < pathPoints.size())
|
||||
{
|
||||
Vector p1 = pathPoints.get(index).toVector();
|
||||
Vector p2 = pathPoints.get(index + 1).toVector();
|
||||
lookDir = p1.add(p2).multiply(0.5).subtract(currentPos);
|
||||
}
|
||||
else
|
||||
lookDir = pathPoints.get(Math.min(index, pathPoints.size() - 1)).toVector().subtract(currentPos);
|
||||
|
||||
Vector horizontalLook = new Vector(lookDir.getX(), 0, lookDir.getZ());
|
||||
if(horizontalLook.lengthSquared() < 1e-6)
|
||||
horizontalLook.copy(previousMoveDir.clone());
|
||||
|
||||
float targetYaw = (float) (Math.toDegrees(Math.atan2(horizontalLook.getZ(), horizontalLook.getX())) - 90);
|
||||
targetYaw = normalizeAngle(targetYaw);
|
||||
|
||||
float diff = normalizeAngle(targetYaw - previousYaw);
|
||||
diff = Math.max(-15f, Math.min(15f, diff));
|
||||
|
||||
float yaw = previousYaw + diff;
|
||||
previousYaw = yaw;
|
||||
previousMoveDir.copy(horizontalLook);
|
||||
|
||||
Vector targetVec = pathPoints.get(Math.min(index + 1, pathPoints.size() - 1)).toVector().subtract(currentPos);
|
||||
double hLen = Math.sqrt(targetVec.getX() * targetVec.getX() + targetVec.getZ() * targetVec.getZ());
|
||||
float pitch = (float) (-Math.toDegrees(Math.atan2(targetVec.getY(), hLen))) / 1.5f;
|
||||
|
||||
return new float[]{yaw, pitch};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an angle to be between -180 and 180 degrees.
|
||||
*
|
||||
* @param angle The angle to normalize
|
||||
* @return The normalized angle
|
||||
*/
|
||||
private float normalizeAngle(float angle)
|
||||
{
|
||||
while(angle > 180)
|
||||
angle -= 360;
|
||||
while(angle < -180)
|
||||
angle += 360;
|
||||
return angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this task allows automatic walking viewer management.
|
||||
*
|
||||
* @return true if viewers can be added automatically; false otherwise
|
||||
*/
|
||||
public boolean isAutoManageWalkingViewers()
|
||||
{
|
||||
return autoManageWalkingViewers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NPC's current walking location.
|
||||
*
|
||||
* @return the current walking location
|
||||
*/
|
||||
public @NotNull Location getCurrentLocation()
|
||||
{
|
||||
World world = npc.getLocation().getWorld();
|
||||
return currentPos.toLocation(world);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a viewer to this path task and immediately syncs the NPC's current walking position.
|
||||
*
|
||||
* @param player the player to add
|
||||
* @return true if the player was newly added; false if the player was already a viewer
|
||||
*/
|
||||
public boolean addViewer(@NotNull Player player)
|
||||
{
|
||||
if(finished)
|
||||
return false;
|
||||
|
||||
if(!viewerIds.add(player.getUniqueId()))
|
||||
return false;
|
||||
|
||||
sendCurrentPosition(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified player is already attached to this path task.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @return true if the player is already a viewer of this walking task; false otherwise
|
||||
*/
|
||||
public boolean hasViewer(@NotNull Player player)
|
||||
{
|
||||
return viewerIds.contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a viewer from this path task.
|
||||
*
|
||||
* @param player the player to remove
|
||||
*/
|
||||
public void removeViewer(@NotNull Player player)
|
||||
{
|
||||
viewerIds.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all online viewers currently attached to this path task.
|
||||
*
|
||||
* @return online viewers
|
||||
*/
|
||||
private Player @NotNull [] getViewers()
|
||||
{
|
||||
return viewerIds.stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(Player[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the NPC's current walking position and rotation to a viewer.
|
||||
*
|
||||
* @param player the viewer to sync
|
||||
*/
|
||||
private void sendCurrentPosition(@NotNull Player player)
|
||||
{
|
||||
|
||||
if(serverEntity == null)
|
||||
return;
|
||||
|
||||
RotateHeadPacket head = new RotateHeadPacket(serverEntity, (byte) (previousYaw * 256 / 360));
|
||||
|
||||
Vector currentVec = new Vector(currentPos.getX(), currentPos.getY(), currentPos.getZ());
|
||||
|
||||
TeleportEntityPacket teleport = new TeleportEntityPacket(serverEntity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(currentVec, new Vector(0, 0, 0), previousYaw, previousPitch), Set.of(), true);
|
||||
|
||||
npc.sendNpcMovePackets(teleport, head, player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends movement and rotation packets to update the NPC's position for viewers.
|
||||
*
|
||||
* @param movement The movement vector
|
||||
* @param yaw The yaw rotation
|
||||
* @param pitch The pitch rotation
|
||||
* @param onGround Whether the NPC is on the ground
|
||||
*/
|
||||
private void sendMovePackets(Vector movement, float yaw, float pitch, boolean onGround)
|
||||
{
|
||||
if(serverEntity == null)
|
||||
return;
|
||||
|
||||
previousYaw = yaw;
|
||||
previousPitch = pitch;
|
||||
|
||||
RotateHeadPacket head = new RotateHeadPacket(serverEntity, (byte) (yaw * 256 / 360));
|
||||
TeleportEntityPacket teleport = new TeleportEntityPacket(serverEntity, new TeleportEntityPacket.PositionMoveRotation(currentPos, movement, yaw, pitch),
|
||||
Set.of(), onGround);
|
||||
|
||||
npc.sendNpcMovePackets(teleport, head, getViewers());
|
||||
|
||||
if(updateRealLocation)
|
||||
npc.setLocation(currentPos.toLocation(npc.getLocation().getWorld()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the path task and cleans up resources. Calls the callback with CANCELLED status if not already finished.
|
||||
*
|
||||
* @throws IllegalStateException if the task was already canceled
|
||||
*/
|
||||
@Override
|
||||
public synchronized void cancel() throws IllegalStateException
|
||||
{
|
||||
if(finished)
|
||||
{
|
||||
super.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
finished = true;
|
||||
forceCloseAllDoors();
|
||||
super.cancel();
|
||||
|
||||
if(callback != null)
|
||||
callback.accept(WalkingResult.CANCELLED);
|
||||
|
||||
NpcStopWalkingEvent event = new NpcStopWalkingEvent(npc, WalkingResult.CANCELLED, updateRealLocation);
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
|
||||
if(event.changeRealLocation())
|
||||
{
|
||||
World world = path.getWaypoints().isEmpty() ? pathPoints.get(pathPoints.size() - 1).getWorld() :
|
||||
path.getWaypoints().get(path.getWaypoints().size() - 1).getWorld();
|
||||
Location loc = new Location(world, currentPos.getX(), currentPos.getY(), currentPos.getZ());
|
||||
npc.changeRealLocation(loc, getViewers());
|
||||
}
|
||||
npc.clearWalkingTask(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the path task has been completed.
|
||||
*
|
||||
* @return true if the task is finished, false otherwise
|
||||
*/
|
||||
public boolean isFinished()
|
||||
{
|
||||
return finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* A record representing the result of physics calculations.
|
||||
*
|
||||
* @param yChange The vertical movement to apply
|
||||
* @param isGrounded Whether the NPC is on the ground
|
||||
* @param skipHorizontal Whether to skip horizontal movement this tick
|
||||
* @param horizontalSlowdown Factor to slow down horizontal movement (1.0 = normal, <1.0 = slower)
|
||||
*/
|
||||
private record PhysicsResult(double yChange, boolean isGrounded, boolean skipHorizontal, double horizontalSlowdown) {}
|
||||
|
||||
// --- Builder Class ---
|
||||
|
||||
/**
|
||||
* Builder class for creating PathTask instances with a fluent API. Allows for optional configuration of the path task.
|
||||
*/
|
||||
public static class Builder
|
||||
{
|
||||
private final NPC npc;
|
||||
private final Path path;
|
||||
|
||||
private Player[] viewers = null;
|
||||
private Consumer<WalkingResult> callback = null;
|
||||
private double speed = 1.0;
|
||||
private boolean updateRealLocation = false;
|
||||
private boolean withRotation = true;
|
||||
private boolean autoManageWalkingViewers = false;
|
||||
|
||||
/**
|
||||
* Creates a new Builder for a PathTask.
|
||||
*
|
||||
* @param npc The NPC that will follow the path
|
||||
* @param path The path for the NPC to follow
|
||||
*/
|
||||
public Builder(@NotNull NPC npc, @NotNull Path path)
|
||||
{
|
||||
this.npc = npc;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the viewers who can see the NPC's movement.
|
||||
*
|
||||
* @param viewers Array of players who can see the NPC
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public @NotNull Builder viewers(@Nullable Player... viewers)
|
||||
{
|
||||
this.viewers = viewers;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this path task should allow automatic walking viewer management.
|
||||
* <p>
|
||||
* When enabled, external library listeners may add eligible players to this task while the NPC is already walking.
|
||||
*
|
||||
* @param autoManageWalkingViewers true to allow automatic walking viewer management, false otherwise
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public @NotNull Builder autoManageWalkingViewers(boolean autoManageWalkingViewers)
|
||||
{
|
||||
this.autoManageWalkingViewers = autoManageWalkingViewers;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the movement speed of the NPC.
|
||||
*
|
||||
* @param speed The movement speed (blocks per tick)
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public @NotNull Builder speed(double speed)
|
||||
{
|
||||
this.speed = speed;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to update the NPC's actual location after movement.
|
||||
*
|
||||
* @param update true to update the NPC's real location, false otherwise
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public @NotNull Builder updateRealLocation(boolean update)
|
||||
{
|
||||
this.updateRealLocation = update;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback to be executed when the path is completed or canceled.
|
||||
*
|
||||
* @param callback The callback to execute
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public @NotNull Builder callback(@Nullable Consumer<WalkingResult> callback)
|
||||
{
|
||||
this.callback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to include rotation packets in the movement.
|
||||
*
|
||||
* @param withRotation true to include rotation packets, false otherwise
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public @NotNull Builder withRotation(boolean withRotation)
|
||||
{
|
||||
this.withRotation = withRotation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a new PathTask instance.
|
||||
*
|
||||
* @return A new PathTask with the configured settings
|
||||
*/
|
||||
public @NotNull PathTask build()
|
||||
{
|
||||
return new PathTask(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,31 @@ import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.objects.NpcSkin;
|
||||
import de.eisi05.npc.api.objects.Skin;
|
||||
import de.eisi05.npc.api.utils.Reflections;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* The {@link Tasks} class manages and starts various recurring tasks
|
||||
* related to Non-Player Characters (NPCs) within the Bukkit environment.
|
||||
* These tasks often involve NPC behavior such as looking at nearby players.
|
||||
* The {@link Tasks} class manages and starts various recurring tasks related to Non-Player Characters (NPCs) within the Bukkit environment. These tasks often
|
||||
* involve NPC behavior such as looking at nearby players.
|
||||
*/
|
||||
public class Tasks
|
||||
{
|
||||
private static final Map<UUID, String> placeholderCache = new HashMap<>();
|
||||
public static final Map<UUID, Map<UUID, String>> placeholderCache = new ConcurrentHashMap<>();
|
||||
private static final List<CompletableFuture<?>> activeFutures = new ArrayList<>();
|
||||
private static BukkitTask lookAtTask;
|
||||
private static BukkitTask placeholderTask;
|
||||
|
||||
/**
|
||||
* Starts all defined NPC-related tasks.
|
||||
* This method should be called when the plugin is enabled to ensure
|
||||
* that NPC behaviors are active.
|
||||
* Starts all defined NPC-related tasks. This method should be called when the plugin is enabled to ensure that NPC behaviors are active.
|
||||
*/
|
||||
public static void start()
|
||||
{
|
||||
@@ -47,13 +47,18 @@ public class Tasks
|
||||
|
||||
if(placeholderTask != null && !placeholderTask.isCancelled())
|
||||
placeholderTask.cancel();
|
||||
|
||||
synchronized(activeFutures)
|
||||
{
|
||||
for(CompletableFuture<?> future : activeFutures)
|
||||
future.cancel(true);
|
||||
activeFutures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a recurring task that makes NPCs look at nearby players.
|
||||
* The task runs on a timer defined by {@code NpcApi.config.lookAtTimer()}.
|
||||
* NPCs will only look at players within a specified range, which is
|
||||
* configured via {@link NpcOption#LOOK_AT_PLAYER}.
|
||||
* Implements a recurring task that makes NPCs look at nearby players. The task runs on a timer defined by {@code NpcApi.config.lookAtTimer()}. NPCs will
|
||||
* only look at players within a specified range, which is configured via {@link NpcOption#LOOK_AT_PLAYER}.
|
||||
*/
|
||||
private static void lookAtTask()
|
||||
{
|
||||
@@ -62,24 +67,23 @@ public class Tasks
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
NpcManager.getList().forEach(npc ->
|
||||
for(NPC npc : NpcManager.getList())
|
||||
{
|
||||
double range = npc.getOption(NpcOption.LOOK_AT_PLAYER);
|
||||
|
||||
if(range <= 0)
|
||||
return;
|
||||
continue;
|
||||
|
||||
npc.getServerPlayer().getBukkitPlayer().getNearbyEntities(range, range, range)
|
||||
npc.entity.getBukkitPlayer().getNearbyEntities(range, range, range)
|
||||
.stream().filter(entity -> entity instanceof Player)
|
||||
.forEach(entity -> npc.lookAtPlayer((Player) entity));
|
||||
});
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(NpcApi.plugin, 0, NpcApi.config.lookAtTimer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a recurring task that updates placeholders.
|
||||
* The task runs on a timer defined by {@code NpcApi.config.placeholderTimer()}.
|
||||
* Implements a recurring task that updates placeholders. The task runs on a timer defined by {@code NpcApi.config.placeholderTimer()}.
|
||||
*/
|
||||
private static void placeholderTask()
|
||||
{
|
||||
@@ -88,33 +92,90 @@ public class Tasks
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
NpcManager.getList().stream().filter(npc -> !npc.getNpcName().isStatic()).forEach(NPC::updateNameForAll);
|
||||
Collection<NPC> npcs = NpcManager.getList();
|
||||
for(NPC npc : npcs)
|
||||
{
|
||||
if(!npc.getNpcName().isStatic())
|
||||
npc.updateNameForAll();
|
||||
}
|
||||
|
||||
if(!Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI"))
|
||||
return;
|
||||
|
||||
NpcManager.getList().forEach(npc ->
|
||||
for(NPC npc : npcs)
|
||||
{
|
||||
NpcSkin npcSkin = npc.getOption(NpcOption.SKIN);
|
||||
if(npcSkin == null || npcSkin.isStatic() || npcSkin.getSkin() == null)
|
||||
return;
|
||||
|
||||
npc.updateSkin(player ->
|
||||
for(UUID viewerId : npc.getViewers())
|
||||
{
|
||||
String newPlaceholder = (String) Reflections.invokeStaticMethod("me.clip.placeholderapi.PlaceholderAPI",
|
||||
"setPlaceholders", player, npcSkin.getSkin().name()).get();
|
||||
if(viewerId == null)
|
||||
continue;
|
||||
|
||||
String old = placeholderCache.getOrDefault(player.getUniqueId(), "");
|
||||
Player player = Bukkit.getPlayer(viewerId);
|
||||
if(player == null)
|
||||
continue;
|
||||
|
||||
if(!old.equals(newPlaceholder))
|
||||
{
|
||||
placeholderCache.put(player.getUniqueId(), newPlaceholder);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
NpcSkin npcSkin = npc.getOption(NpcOption.SKIN, player);
|
||||
if(npcSkin == null || npcSkin.isStatic() || npcSkin.getPlaceholder() == null || npc.getOption(NpcOption.USE_PLAYER_SKIN, player))
|
||||
continue;
|
||||
|
||||
updateSkin(player, npc, npcSkin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(NpcApi.plugin, NpcApi.config.placeholderTimer(), NpcApi.config.placeholderTimer());
|
||||
}.runTaskTimer(NpcApi.plugin, 10, NpcApi.config.placeholderTimer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the skin of an NPC for a specific player based on a placeholder value. This method handles both UUID and string-based skin lookups, and updates
|
||||
* the NPC's skin asynchronously when the skin is fetched.
|
||||
*
|
||||
* @param player The player who will see the updated skin. Must not be null.
|
||||
* @param npc The NPC whose skin will be updated. Must not be null.
|
||||
* @param npcSkin The NpcSkin configuration containing the placeholder and skin settings. Must not be null.
|
||||
* @throws NullPointerException if any parameter is null.
|
||||
*/
|
||||
public static void updateSkin(@NotNull Player player, @NotNull NPC npc, @NotNull NpcSkin npcSkin)
|
||||
{
|
||||
String newPlaceholder = (String) Reflections.invokeStaticMethod("me.clip.placeholderapi.PlaceholderAPI", "setPlaceholders", player,
|
||||
npcSkin.getPlaceholder()).get();
|
||||
|
||||
Map<UUID, String> playerCache = placeholderCache.getOrDefault(npc.getUUID(), new ConcurrentHashMap<>());
|
||||
String oldPlaceholder = playerCache.getOrDefault(player.getUniqueId(), null);
|
||||
if(newPlaceholder.equals(oldPlaceholder))
|
||||
return;
|
||||
|
||||
playerCache.put(player.getUniqueId(), newPlaceholder);
|
||||
placeholderCache.put(npc.getUUID(), playerCache);
|
||||
|
||||
try
|
||||
{
|
||||
UUID skinUuid = UUID.fromString(newPlaceholder);
|
||||
Skin.fetchSkinAsync(skinUuid).thenAccept(skinOpt -> skinOpt.ifPresent(skin ->
|
||||
Bukkit.getScheduler().runTaskLater(NpcApi.plugin, () -> npc.updateSkin(player), 1)));
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
Skin.fetchSkinAsync(newPlaceholder).thenAccept(skinOpt -> skinOpt.ifPresent(skin ->
|
||||
Bukkit.getScheduler().runTaskLater(NpcApi.plugin, () -> npc.updateSkin(player), 1)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks a CompletableFuture for cancellation on plugin disable.
|
||||
*
|
||||
* @param future the future to track
|
||||
*/
|
||||
public static void trackFuture(@NotNull CompletableFuture<?> future)
|
||||
{
|
||||
synchronized(activeFutures)
|
||||
{
|
||||
activeFutures.add(future);
|
||||
}
|
||||
future.whenComplete((result, error) ->
|
||||
{
|
||||
synchronized(activeFutures)
|
||||
{
|
||||
activeFutures.remove(future);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Indicates that a configuration option or field is strictly reserved for third-party API integration and is not actively utilized by the internal plugin
|
||||
* logic.
|
||||
* <p/>
|
||||
* {@link de.eisi05.npc.api.objects.NpcConfig}
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({ElementType.FIELD, ElementType.METHOD})
|
||||
public @interface ApiOnly
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.OptionalInt;
|
||||
|
||||
/**
|
||||
* Utility class for location-related operations.
|
||||
*/
|
||||
public class LocationUtils
|
||||
{
|
||||
/**
|
||||
* Finds a safe Y level on the ground for the given location.
|
||||
* Searches up to 5 blocks above and below the starting Y level.
|
||||
*
|
||||
* @param loc The location to find a safe Y level for
|
||||
* @return An OptionalInt containing the safe Y level, or empty if none found
|
||||
*/
|
||||
public static OptionalInt findSafeY(@NotNull Location loc)
|
||||
{
|
||||
World world = loc.getWorld();
|
||||
if(world == null)
|
||||
return OptionalInt.empty();
|
||||
|
||||
int startX = loc.getBlockX();
|
||||
int startZ = loc.getBlockZ();
|
||||
int startY = loc.getBlockY();
|
||||
|
||||
for(int y = startY; y <= startY + 5; y++)
|
||||
{
|
||||
Block block = world.getBlockAt(startX, y, startZ);
|
||||
if(block.getType().isSolid() && !block.getRelative(BlockFace.UP).getType().isSolid() &&
|
||||
!block.getRelative(BlockFace.UP).getRelative(BlockFace.UP).getType().isSolid())
|
||||
return OptionalInt.of(y + 1);
|
||||
}
|
||||
|
||||
for(int y = startY; y >= startY - 5; y--)
|
||||
{
|
||||
Block block = world.getBlockAt(startX, y, startZ);
|
||||
if(block.getType().isSolid() && !block.getRelative(BlockFace.UP).getType().isSolid() &&
|
||||
!block.getRelative(BlockFace.UP).getRelative(BlockFace.UP).getType().isSolid())
|
||||
return OptionalInt.of(y + 1);
|
||||
}
|
||||
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,181 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.RayTraceResult;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class NpcHitboxUtil
|
||||
{
|
||||
/**
|
||||
* Checks whether a player's line of sight intersects the Oriented Bounding Box (OBB) of an NPC using an optimized RaySlab intersection algorithm.
|
||||
* <p>
|
||||
* This method accounts for the NPC's custom scale, bounding box dimensions, and spatial orientation (yaw rotation). It supports specialized poses such as
|
||||
* {@link org.bukkit.entity.Pose#SLEEPING}, adjusting the bounding box extents and center offset dynamically to match the modified client-side model.
|
||||
* </p>
|
||||
* <p>
|
||||
* To prevent clipping exploits, the method executes a fast-fail line-of-sight raytrace via Bukkit's world physics engine up to the point of structural
|
||||
* intersection, ensuring that solid blocks or other valid entities do not obstruct the interaction.
|
||||
* </p>
|
||||
*
|
||||
* @param npc the {@link NPC} instance whose bounding box is being evaluated
|
||||
* @param sleeping {@code true} if the NPC is currently rendered in a sleeping pose, triggering specialized horizontal bounding box transformations
|
||||
* @param player the {@link Player} executing the interaction, used to determine ray origin, direction vector, and maximum interaction range attributes
|
||||
* @return {@code true} if the player's ray safely intersects the NPC's bounding box within their maximum interaction range without being obstructed by
|
||||
* blocks or other entities; {@code false} otherwise * @see org.bukkit.util.Vector
|
||||
* @see org.bukkit.util.RayTraceResult
|
||||
*/
|
||||
public static boolean rayIntersectsNpc(@NotNull NPC npc, boolean sleeping, @NotNull Player player)
|
||||
{
|
||||
Location eyeLocation = player.getEyeLocation();
|
||||
Vector origin = eyeLocation.toVector();
|
||||
Vector dir = eyeLocation.getDirection();
|
||||
|
||||
double maxDistance;
|
||||
try
|
||||
{
|
||||
maxDistance = player.getAttribute(Attribute.valueOf("GENERIC_ENTITY_INTERACTION_RANGE")).getValue();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
maxDistance = player.getGameMode() == GameMode.CREATIVE ? 5.0 : 3.0;
|
||||
}
|
||||
|
||||
Location base = npc.getLocation();
|
||||
float yaw = base.getYaw();
|
||||
double yawRad = Math.toRadians(yaw);
|
||||
double cos = Math.cos(yawRad);
|
||||
double sin = Math.sin(yawRad);
|
||||
|
||||
WrappedEntity.BoundingBox bb = npc.entity.getDefaultBoundingBox();
|
||||
double scale = npc.getOption(NpcOption.SCALE, player);
|
||||
|
||||
double width, height, length;
|
||||
Vector centerOffset = new Vector(0, 0, 0);
|
||||
|
||||
if(sleeping)
|
||||
{
|
||||
width = bb.getXSize() * scale;
|
||||
height = bb.getZSize() * scale;
|
||||
length = bb.getYSize() * scale;
|
||||
|
||||
// Inlined forward vector rotation to save allocations
|
||||
Vector forward = new Vector(-sin, 0, cos).normalize();
|
||||
centerOffset.copy(forward).multiply(-length / 2.0);
|
||||
centerOffset.setY(height / 2.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
width = bb.getXSize() * scale;
|
||||
height = bb.getYSize() * scale;
|
||||
length = bb.getZSize() * scale;
|
||||
centerOffset.setY(height / 2.0);
|
||||
}
|
||||
|
||||
Vector center = base.toVector().add(centerOffset);
|
||||
Vector oc = origin.clone().subtract(center);
|
||||
|
||||
// Inverse rotation to bring ray into local OBB space
|
||||
double ox = oc.getX() * cos + oc.getZ() * sin;
|
||||
double oz = -oc.getX() * sin + oc.getZ() * cos;
|
||||
double oy = oc.getY();
|
||||
|
||||
double dx = dir.getX() * cos + dir.getZ() * sin;
|
||||
double dz = -dir.getX() * sin + dir.getZ() * cos;
|
||||
double dy = dir.getY();
|
||||
|
||||
double minX = -width / 2.0, maxX = width / 2.0;
|
||||
double minY = -height / 2.0, maxY = height / 2.0;
|
||||
double minZ = -length / 2.0, maxZ = length / 2.0;
|
||||
|
||||
double tMin = 0.0;
|
||||
double tMax = maxDistance;
|
||||
|
||||
// X Slab
|
||||
if(Math.abs(dx) < 1e-8)
|
||||
{
|
||||
if(ox < minX || ox > maxX)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double invDx = 1.0 / dx;
|
||||
double t1 = (minX - ox) * invDx;
|
||||
double t2 = (maxX - ox) * invDx;
|
||||
if(t1 > t2)
|
||||
{
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
tMin = Math.max(tMin, t1);
|
||||
tMax = Math.min(tMax, t2);
|
||||
if(tMin > tMax)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Y Slab
|
||||
if(Math.abs(dy) < 1e-8)
|
||||
{
|
||||
if(oy < minY || oy > maxY)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double invDy = 1.0 / dy;
|
||||
double t1 = (minY - oy) * invDy;
|
||||
double t2 = (maxY - oy) * invDy;
|
||||
if(t1 > t2)
|
||||
{
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
tMin = Math.max(tMin, t1);
|
||||
tMax = Math.min(tMax, t2);
|
||||
if(tMin > tMax)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Z Slab
|
||||
if(Math.abs(dz) < 1e-8)
|
||||
{
|
||||
if(oz < minZ || oz > maxZ)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double invDz = 1.0 / dz;
|
||||
double t1 = (minZ - oz) * invDz;
|
||||
double t2 = (maxZ - oz) * invDz;
|
||||
if(t1 > t2)
|
||||
{
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
tMin = Math.max(tMin, t1);
|
||||
tMax = Math.min(tMax, t2);
|
||||
if(tMin > tMax)
|
||||
return false;
|
||||
}
|
||||
|
||||
RayTraceResult blockHit = eyeLocation.getWorld().rayTraceBlocks(eyeLocation, dir, tMin, FluidCollisionMode.NEVER, true);
|
||||
if(blockHit != null && blockHit.getHitBlock() != null)
|
||||
return false;
|
||||
|
||||
RayTraceResult entityHit = eyeLocation.getWorld().rayTraceEntities(eyeLocation, dir, tMin, 0.1,
|
||||
(entity) -> !entity.equals(player) && entity.isValid());
|
||||
if(entityHit != null && entityHit.getHitEntity() != null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,17 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The {@link ObjectSaver} class provides utility methods for serializing and deserializing
|
||||
* Java objects and lists of objects to and from a file. It uses standard Java
|
||||
* object serialization.
|
||||
* The {@link ObjectSaver} class provides utility methods for serializing and deserializing Java objects and lists of objects to and from a file. It uses
|
||||
* standard Java object serialization.
|
||||
*/
|
||||
public class ObjectSaver
|
||||
{
|
||||
private final static ObjectInputFilter objectInputFilter = filterInfo -> ObjectInputFilter.Status.ALLOWED;
|
||||
|
||||
private final File file;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ObjectSaver} instance, creating a {@link File} object
|
||||
* from the given file path string. It ensures the parent directories exist
|
||||
* Constructs a new {@code ObjectSaver} instance, creating a {@link File} object from the given file path string. It ensures the parent directories exist
|
||||
* and creates the file if it doesn't already exist.
|
||||
*
|
||||
* @param file The path to the file where objects will be saved/loaded. Must not be {@code null}.
|
||||
@@ -30,9 +30,8 @@ public class ObjectSaver
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ObjectSaver} instance with the specified {@link File} object.
|
||||
* It ensures the parent directories of the file exist and creates the file itself
|
||||
* if it does not already exist.
|
||||
* Constructs a new {@code ObjectSaver} instance with the specified {@link File} object. It ensures the parent directories of the file exist and creates the
|
||||
* file itself if it does not already exist.
|
||||
*
|
||||
* @param file The {@link File} object where objects will be saved/loaded. Must not be {@code null}.
|
||||
* @throws RuntimeException If an {@link IOException} occurs while creating the file.
|
||||
@@ -46,7 +45,8 @@ public class ObjectSaver
|
||||
try
|
||||
{
|
||||
file.createNewFile();
|
||||
} catch(IOException e)
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -54,8 +54,8 @@ public class ObjectSaver
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single serializable object to the file, appending to the file if it already exists.
|
||||
* This is a convenience method that calls {@link #write(Serializable, boolean)} with {@code append} set to {@code true}.
|
||||
* Writes a single serializable object to the file, appending to the file if it already exists. This is a convenience method that calls
|
||||
* {@link #write(Serializable, boolean)} with {@code append} set to {@code true}.
|
||||
*
|
||||
* @param object The object to write to the file. Must implement {@link Serializable}.
|
||||
* @param <T> The type of the object, which must extend {@link Serializable}.
|
||||
@@ -63,15 +63,14 @@ public class ObjectSaver
|
||||
*/
|
||||
public <T extends Serializable> void write(T object) throws IOException
|
||||
{
|
||||
this.write(object, true);
|
||||
this.write(object, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single serializable object to the file.
|
||||
*
|
||||
* @param object The object to write to the file. Must implement {@link Serializable}.
|
||||
* @param append If {@code true}, the object will be appended to the end of the file.
|
||||
* If {@code false}, the file will be truncated (its contents deleted)
|
||||
* @param append If {@code true}, the object will be appended to the end of the file. If {@code false}, the file will be truncated (its contents deleted)
|
||||
* before writing the new object.
|
||||
* @param <T> The type of the object, which must extend {@link Serializable}.
|
||||
* @throws IOException If an I/O error occurs during writing.
|
||||
@@ -85,8 +84,8 @@ public class ObjectSaver
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a list of serializable objects to the file, appending to the file if it already exists.
|
||||
* This is a convenience method that calls {@link #writeList(List, boolean)} with {@code append} set to {@code true}.
|
||||
* Writes a list of serializable objects to the file, appending to the file if it already exists. This is a convenience method that calls
|
||||
* {@link #writeList(List, boolean)} with {@code append} set to {@code true}.
|
||||
*
|
||||
* @param object The list of objects to write to the file. Each object in the list must implement {@link Serializable}. Must not be {@code null}.
|
||||
* @param <T> The type of the objects in the list, which must extend {@link Serializable}.
|
||||
@@ -94,15 +93,14 @@ public class ObjectSaver
|
||||
*/
|
||||
public <T extends Serializable> void writeList(@NotNull List<T> object) throws IOException
|
||||
{
|
||||
this.writeList(object, true);
|
||||
this.writeList(object, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a list of serializable objects to the file.
|
||||
*
|
||||
* @param object The list of objects to write to the file. Each object in the list must implement {@link Serializable}. Must not be {@code null}.
|
||||
* @param append If {@code true}, the list will be appended to the end of the file.
|
||||
* If {@code false}, the file will be truncated (its contents deleted)
|
||||
* @param append If {@code true}, the list will be appended to the end of the file. If {@code false}, the file will be truncated (its contents deleted)
|
||||
* before writing the new list.
|
||||
* @param <T> The type of the objects in the list, which must extend {@link Serializable}.
|
||||
* @throws IOException If an I/O error occurs during writing.
|
||||
@@ -119,8 +117,7 @@ public class ObjectSaver
|
||||
* Reads a single serializable object from the file.
|
||||
*
|
||||
* @param <T> The expected type of the object, which must extend {@link Serializable}.
|
||||
* @return The deserialized object, or {@code null} if an error occurs during reading
|
||||
* (e.g., file not found, EOF, class not found, or I/O error).
|
||||
* @return The deserialized object, or {@code null} if an error occurs during reading (e.g., file not found, EOF, class not found, or I/O error).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Serializable> @Nullable T read()
|
||||
@@ -129,10 +126,12 @@ public class ObjectSaver
|
||||
{
|
||||
FileInputStream fileIn = new FileInputStream(this.file);
|
||||
ObjectInputStream objectOut = new ObjectInputStream(fileIn);
|
||||
objectOut.setObjectInputFilter(objectInputFilter);
|
||||
Object object = objectOut.readObject();
|
||||
objectOut.close();
|
||||
return (T) object;
|
||||
} catch(IOException | ClassNotFoundException e)
|
||||
}
|
||||
catch(IOException | ClassNotFoundException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -142,8 +141,8 @@ public class ObjectSaver
|
||||
* Reads a list of serializable objects from the file.
|
||||
*
|
||||
* @param <T> The expected type of the objects in the list, which must extend {@link Serializable}.
|
||||
* @return The deserialized list of objects. Returns an empty {@link ArrayList} if an error occurs
|
||||
* during reading (e.g., file not found, EOF, class not found, or I/O error), or if the file is empty.
|
||||
* @return The deserialized list of objects. Returns an empty {@link ArrayList} if an error occurs during reading (e.g., file not found, EOF, class not
|
||||
* found, or I/O error), or if the file is empty.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Serializable> @NotNull List<T> readList()
|
||||
@@ -152,10 +151,12 @@ public class ObjectSaver
|
||||
{
|
||||
FileInputStream fileIn = new FileInputStream(this.file);
|
||||
ObjectInputStream objectOut = new ObjectInputStream(fileIn);
|
||||
objectOut.setObjectInputFilter(objectInputFilter);
|
||||
Object object = objectOut.readObject();
|
||||
objectOut.close();
|
||||
return (List<T>) object;
|
||||
} catch(Exception var4)
|
||||
}
|
||||
catch(Exception var4)
|
||||
{
|
||||
return new ArrayList();
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import de.eisi05.npc.api.events.NpcInteractEvent;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.wrapper.enums.InteractionHand;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedMinecraftServer;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
|
||||
import de.eisi05.npc.api.wrapper.packets.PacketWrapper;
|
||||
import de.eisi05.npc.api.wrapper.packets.UseEntityPacketWrapper;
|
||||
import de.eisi05.npc.api.wrapper.packets.*;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -17,26 +17,24 @@ import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* The {@link PacketReader} class is responsible for injecting a custom Netty
|
||||
* channel handler into a player's network pipeline to intercept incoming packets.
|
||||
* It specifically listens for packets related to entity interaction (e.g., attacking or interacting with NPCs)
|
||||
* and dispatches custom events based on these interactions. It also allows for
|
||||
* custom packet readers to be added.
|
||||
* The {@link PacketReader} class is responsible for injecting a custom Netty channel handler into a player's network pipeline to intercept incoming packets. It
|
||||
* specifically listens for packets related to entity interaction (e.g., attacking or interacting with NPCs) and dispatches custom events based on these
|
||||
* interactions. It also allows for custom packet readers to be added.
|
||||
*/
|
||||
public class PacketReader
|
||||
{
|
||||
private static final Map<UUID, Channel> channels = new HashMap<>();
|
||||
private static final List<BiConsumer<Player, Object>> readers = new ArrayList<>();
|
||||
private static final Map<UUID, Integer> cancelUseUntilTick = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Adds a custom packet reader to the list of readers.
|
||||
* This reader will be called for every incoming packet processed by the injected handler.
|
||||
* Adds a custom packet reader to the list of readers. This reader will be called for every incoming packet processed by the injected handler.
|
||||
*
|
||||
* @param reader The {@link BiConsumer} to add. It accepts the {@link Player}
|
||||
* and the raw packet {@link Object}. Must not be {@code null}.
|
||||
* @param reader The {@link BiConsumer} to add. It accepts the {@link Player} and the raw packet {@link Object}. Must not be {@code null}.
|
||||
*/
|
||||
public static void addReader(@NotNull BiConsumer<Player, Object> reader)
|
||||
{
|
||||
@@ -44,9 +42,8 @@ public class PacketReader
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a custom {@link ChannelDuplexHandler} into the specified player's Netty pipeline.
|
||||
* This handler intercepts incoming packets to check for NPC interactions.
|
||||
* The handler is named after the plugin's name to avoid conflicts and ensure proper removal.
|
||||
* Injects a custom {@link ChannelDuplexHandler} into the specified player's Netty pipeline. This handler intercepts incoming packets to check for NPC
|
||||
* interactions. The handler is named after the plugin's name to avoid conflicts and ensure proper removal.
|
||||
*
|
||||
* @param player The {@link Player} whose pipeline is to be injected. Must not be {@code null}.
|
||||
*/
|
||||
@@ -55,6 +52,10 @@ public class PacketReader
|
||||
WrappedServerPlayer serverPlayer = WrappedServerPlayer.fromPlayer(player);
|
||||
|
||||
Channel channel = serverPlayer.playerConnection().networkManager().channel();
|
||||
|
||||
if(channel == null || NpcApi.plugin == null)
|
||||
return;
|
||||
|
||||
channels.put(player.getUniqueId(), channel);
|
||||
|
||||
if(channel.pipeline().get(NpcApi.plugin.getName()) != null)
|
||||
@@ -80,9 +81,8 @@ public class PacketReader
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given packet is a {@link UseEntityPacketWrapper} and, if so,
|
||||
* processes the interaction to dispatch a {@link NpcInteractEvent}.
|
||||
* This method is responsible for determining if a player has clicked or attacked an NPC.
|
||||
* Checks if the given packet is a {@link UseEntityPacketWrapper} and, if so, processes the interaction to dispatch a {@link NpcInteractEvent}. This method
|
||||
* is responsible for determining if a player has clicked or attacked an NPC.
|
||||
*
|
||||
* @param packet The raw packet object received from the Netty pipeline. Must not be {@code null}.
|
||||
* @param player The {@link Player} who sent the packet. Must not be {@code null}.
|
||||
@@ -92,6 +92,35 @@ public class PacketReader
|
||||
if(!PacketWrapper.PacketHolder.is(packet, PacketWrapper.PacketHolder.class))
|
||||
return;
|
||||
|
||||
if(PacketWrapper.PacketHolder.is(packet, UseItemPacketWrapper.class))
|
||||
{
|
||||
int currentTick = WrappedMinecraftServer.getCurrentTick();
|
||||
Integer until = cancelUseUntilTick.remove(player.getUniqueId());
|
||||
|
||||
if(until == null || currentTick > until)
|
||||
return;
|
||||
|
||||
Bukkit.getScheduler().runTask(NpcApi.plugin, () ->
|
||||
{
|
||||
WrappedServerPlayer serverPlayer = WrappedServerPlayer.fromPlayer(player);
|
||||
serverPlayer.stopUsingItem();
|
||||
serverPlayer.sendPacket(AnimatePacket.create(serverPlayer, AnimatePacket.Animation.SWING_MAIN_HAND));
|
||||
player.updateInventory();
|
||||
});
|
||||
}
|
||||
|
||||
int currentTick = WrappedMinecraftServer.getCurrentTick();
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V26_1) && PacketWrapper.PacketHolder.is(packet, AttackPacket.class))
|
||||
{
|
||||
NPC npc = NpcManager.fromId(PacketWrapper.PacketHolder.wrap(packet, AttackPacket.class).getId()).orElse(null);
|
||||
if(npc == null)
|
||||
return;
|
||||
|
||||
callNpc(player, npc, ClickActionType.LEFT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!PacketWrapper.PacketHolder.is(packet, UseEntityPacketWrapper.class))
|
||||
return;
|
||||
|
||||
@@ -99,26 +128,47 @@ public class PacketReader
|
||||
|
||||
int id = useEntityPacketWrapper.getId();
|
||||
|
||||
NPC npc = NpcManager.getList().stream().filter(npc1 -> npc1.getServerPlayer().getId() == id).findFirst().orElse(null);
|
||||
|
||||
NPC npc = NpcManager.fromId(id).orElse(null);
|
||||
if(npc == null)
|
||||
return;
|
||||
|
||||
UseEntityPacketWrapper.Action action = useEntityPacketWrapper.getAction();
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
{
|
||||
if(useEntityPacketWrapper.usingSecondaryAction() || useEntityPacketWrapper.getHand() == InteractionHand.OFF_HAND)
|
||||
return;
|
||||
|
||||
callNpc(player, npc, ClickActionType.RIGHT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
return;
|
||||
}
|
||||
|
||||
UseEntityPacketWrapper.Action action = useEntityPacketWrapper.getAction();
|
||||
if(action.getActionType() == UseEntityPacketWrapper.ActionType.ATTACK)
|
||||
{
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
|
||||
() -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.LEFT)), 0);
|
||||
callNpc(player, npc, ClickActionType.LEFT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
}
|
||||
else if(action.getActionType() == UseEntityPacketWrapper.ActionType.INTERACT && action.getHand() == InteractionHand.MAIN_HAND)
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
|
||||
() -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.RIGHT)), 0);
|
||||
{
|
||||
callNpc(player, npc, ClickActionType.RIGHT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninjects the custom {@link ChannelDuplexHandler} from the specified player's Netty pipeline.
|
||||
* This stops the interception of packets for that player.
|
||||
* Calls the {@link NpcInteractEvent} for the specified player and NPC.
|
||||
*
|
||||
* @param player The {@link Player} who interacted with the NPC. Must not be {@code null}.
|
||||
* @param npc The {@link NPC} that was interacted with. Must not be {@code null}.
|
||||
* @param type The type of interaction (left or right click). Must not be {@code null}.
|
||||
*/
|
||||
public static void callNpc(@NotNull Player player, @NotNull NPC npc, @NotNull ClickActionType type)
|
||||
{
|
||||
Bukkit.getScheduler().runTask(NpcApi.plugin, () -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninjects the custom {@link ChannelDuplexHandler} from the specified player's Netty pipeline. This stops the interception of packets for that player.
|
||||
*
|
||||
* @param player The {@link Player} whose pipeline is to be uninject. Must not be {@code null}.
|
||||
*/
|
||||
@@ -126,7 +176,7 @@ public class PacketReader
|
||||
{
|
||||
Channel channel = channels.get(player.getUniqueId());
|
||||
|
||||
if(channel == null)
|
||||
if(channel == null || NpcApi.plugin == null)
|
||||
return;
|
||||
|
||||
if(channel.pipeline().get(NpcApi.plugin.getName()) != null)
|
||||
@@ -134,8 +184,7 @@ public class PacketReader
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninjects the custom packet handler from all currently online players.
|
||||
* This is typically called during plugin shutdown or reload.
|
||||
* Uninjects the custom packet handler from all currently online players. This is typically called during plugin shutdown or reload.
|
||||
*/
|
||||
public static void uninjectAll()
|
||||
{
|
||||
@@ -144,8 +193,7 @@ public class PacketReader
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the custom packet handler into all currently online players.
|
||||
* This is typically called during plugin startup or after a reload.
|
||||
* Injects the custom packet handler into all currently online players. This is typically called during plugin startup or after a reload.
|
||||
*/
|
||||
public static void injectAll()
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import com.google.common.primitives.Primitives;
|
||||
import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -10,14 +9,19 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Utility class providing methods for reflective operations such as loading classes,
|
||||
* invoking methods, and accessing fields dynamically at runtime.
|
||||
* Utility class providing methods for reflective operations such as loading classes, invoking methods, and accessing fields dynamically at runtime.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class Reflections
|
||||
{
|
||||
private static final ConcurrentHashMap<MethodKey, Method> METHOD_CACHE = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<FieldKey, Field> FIELD_CACHE = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<ConstructorKey, Constructor<?>> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, Class<?>> CLASS_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Loads a class by its fully qualified name.
|
||||
*
|
||||
@@ -29,8 +33,16 @@ public class Reflections
|
||||
{
|
||||
try
|
||||
{
|
||||
return Optional.of((Class<T>) Class.forName(path));
|
||||
} catch(ClassNotFoundException e)
|
||||
Class<?> cached = CLASS_CACHE.get(path);
|
||||
if(cached != null)
|
||||
return Optional.of((Class<T>) cached);
|
||||
|
||||
Class<?> clazz = Class.forName(path);
|
||||
CLASS_CACHE.put(path, clazz);
|
||||
|
||||
return Optional.of((Class<T>) clazz);
|
||||
}
|
||||
catch(ClassNotFoundException e)
|
||||
{
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -51,46 +63,42 @@ public class Reflections
|
||||
|
||||
public static <T> @NotNull Optional<T> getInstance(@NotNull Class<T> clazz, @Nullable Object... args)
|
||||
{
|
||||
final Object[] newArgs = Wrapper.checkArgs(args);
|
||||
|
||||
try
|
||||
{
|
||||
Class<?>[] argTypes = Arrays.stream(newArgs)
|
||||
.map(Object::getClass)
|
||||
.toArray(Class[]::new);
|
||||
Class<?>[] argTypes = args == null ? new Class<?>[0] : Arrays.stream(args)
|
||||
.map(o -> o == null ? Object.class : o.getClass())
|
||||
.toArray(Class<?>[]::new);
|
||||
|
||||
for(Constructor<?> ctor : clazz.getDeclaredConstructors())
|
||||
ConstructorKey key = new ConstructorKey(clazz, argTypes);
|
||||
Constructor<?> ctor = CONSTRUCTOR_CACHE.get(key);
|
||||
|
||||
if(ctor == null)
|
||||
{
|
||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
||||
if(paramTypes.length != argTypes.length)
|
||||
continue;
|
||||
|
||||
boolean match = true;
|
||||
for(int i = 0; i < paramTypes.length; i++)
|
||||
for(Constructor<?> constructor : clazz.getDeclaredConstructors())
|
||||
{
|
||||
if(!paramTypes[i].isAssignableFrom(argTypes[i]))
|
||||
if(isCompatible(argTypes, constructor.getParameterTypes(), constructor.isVarArgs()))
|
||||
{
|
||||
match = false;
|
||||
ctor = constructor;
|
||||
ctor.setAccessible(true);
|
||||
CONSTRUCTOR_CACHE.put(key, ctor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(match)
|
||||
{
|
||||
ctor.setAccessible(true);
|
||||
return Optional.of((T) ctor.newInstance(newArgs));
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
} catch(Exception e)
|
||||
if(ctor == null)
|
||||
throw new NoSuchMethodException("No compatible constructor found in class " + clazz.getName() + " for arguments " + Arrays.toString(argTypes));
|
||||
|
||||
return Optional.of((T) ctor.newInstance(args));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a declared method in the specified class that matches the given name and parameter types.
|
||||
* Finds a declared method in the specified class that matches the given name and parameter types. Uses caching to avoid repeated reflection overhead.
|
||||
*
|
||||
* @param clazz the class to search in
|
||||
* @param name the name of the method
|
||||
@@ -100,11 +108,16 @@ public class Reflections
|
||||
*/
|
||||
private static @NotNull Method findMethod(@NotNull Class<?> clazz, @NotNull String name, @Nullable Object[] args) throws NoSuchMethodException
|
||||
{
|
||||
Class<?> current = clazz;
|
||||
Class<?>[] argTypes = Arrays.stream(args)
|
||||
.map(Object::getClass)
|
||||
.toArray(Class<?>[]::new);
|
||||
Class<?>[] argTypes = args == null ? new Class<?>[0] : Arrays.stream(args)
|
||||
.map(o -> o == null ? Object.class : o.getClass())
|
||||
.toArray(Class<?>[]::new);
|
||||
|
||||
MethodKey key = new MethodKey(clazz, name, argTypes);
|
||||
Method cached = METHOD_CACHE.get(key);
|
||||
if(cached != null)
|
||||
return cached;
|
||||
|
||||
Class<?> current = clazz;
|
||||
while(current != null)
|
||||
{
|
||||
for(Method method : current.getDeclaredMethods())
|
||||
@@ -118,6 +131,7 @@ public class Reflections
|
||||
if(isCompatible(argTypes, paramTypes, isVarArgs))
|
||||
{
|
||||
method.setAccessible(true);
|
||||
METHOD_CACHE.put(key, method);
|
||||
return method;
|
||||
}
|
||||
}
|
||||
@@ -130,8 +144,7 @@ public class Reflections
|
||||
/**
|
||||
* Checks if a given set of argument types is compatible with the parameter types of a method.
|
||||
* <p>
|
||||
* This method supports both regular and varargs methods. It also handles primitive-to-wrapper
|
||||
* type conversions (e.g., int to Integer).
|
||||
* This method supports both regular and varargs methods. It also handles primitive-to-wrapper type conversions (e.g., int to Integer).
|
||||
*
|
||||
* @param args The types of the provided arguments.
|
||||
* @param params The types of the method's parameters.
|
||||
@@ -180,13 +193,13 @@ public class Reflections
|
||||
*/
|
||||
public static <V> @NotNull ReflectionChain<V> invokeMethod(@NotNull Object object, @NotNull String methodName, @Nullable Object... args)
|
||||
{
|
||||
args = Wrapper.checkArgs(args);
|
||||
try
|
||||
{
|
||||
Method method = findMethod(object.getClass(), methodName, args);
|
||||
method.setAccessible(true);
|
||||
return new ReflectionChain<>((V) method.invoke(object, args));
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -203,21 +216,44 @@ public class Reflections
|
||||
*/
|
||||
public static <V> @NotNull ReflectionChain<V> invokeStaticMethod(@NotNull String classPath, @NotNull String methodName, @Nullable Object... args)
|
||||
{
|
||||
args = Wrapper.checkArgs(args);
|
||||
try
|
||||
{
|
||||
Class<?> clazz = Class.forName(classPath);
|
||||
Method method = findMethod(clazz, methodName, args);
|
||||
method.setAccessible(true);
|
||||
return new ReflectionChain<>((V) method.invoke(null, args));
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a declared field in the specified class by its name and makes it accessible.
|
||||
* Invokes a static method of a class given its fully qualified name.
|
||||
*
|
||||
* @param clazz the class of the method
|
||||
* @param methodName name of the static method
|
||||
* @param args method arguments
|
||||
* @param <V> return type of the method
|
||||
* @return a ReflectionChain wrapping the method's return value
|
||||
*/
|
||||
public static <V> @NotNull ReflectionChain<V> invokeStaticMethod(@NotNull Class<?> clazz, @NotNull String methodName, @Nullable Object... args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Method method = findMethod(clazz, methodName, args);
|
||||
method.setAccessible(true);
|
||||
return new ReflectionChain<>((V) method.invoke(null, args));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a declared field in the specified class by its name and makes it accessible. Uses caching to avoid repeated reflection overhead.
|
||||
*
|
||||
* @param clazz the class to search in
|
||||
* @param fieldName the name of the field
|
||||
@@ -226,14 +262,21 @@ public class Reflections
|
||||
*/
|
||||
private static @NotNull Field findField(@NotNull Class<?> clazz, @NotNull String fieldName) throws NoSuchFieldException
|
||||
{
|
||||
FieldKey key = new FieldKey(clazz, fieldName);
|
||||
Field cached = FIELD_CACHE.get(key);
|
||||
if(cached != null)
|
||||
return cached;
|
||||
|
||||
while(clazz != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
FIELD_CACHE.put(key, field);
|
||||
return field;
|
||||
} catch(NoSuchFieldException ignored)
|
||||
}
|
||||
catch(NoSuchFieldException ignored)
|
||||
{
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
@@ -255,7 +298,8 @@ public class Reflections
|
||||
{
|
||||
Field field = findField(object.getClass(), fieldName);
|
||||
return new ReflectionChain<>((T) field.get(object));
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -276,7 +320,8 @@ public class Reflections
|
||||
{
|
||||
Field field = findField(clazz, fieldName);
|
||||
return (V) field.get(null);
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -297,7 +342,8 @@ public class Reflections
|
||||
Class<?> clazz = Class.forName(classPath);
|
||||
Field field = findField(clazz, fieldName);
|
||||
return (T) field.get(null);
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -316,7 +362,8 @@ public class Reflections
|
||||
{
|
||||
Field field = findField(object.getClass(), fieldName);
|
||||
field.set(object, value);
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -336,12 +383,77 @@ public class Reflections
|
||||
Class<?> clazz = Class.forName(classPath);
|
||||
Field field = findField(clazz, fieldName);
|
||||
field.set(null, value);
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private record MethodKey(Class<?> clazz, String methodName, Class<?>[] paramTypes)
|
||||
{
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
return true;
|
||||
if(!(o instanceof MethodKey methodKey))
|
||||
return false;
|
||||
return clazz.equals(methodKey.clazz) && methodName.equals(methodKey.methodName) && Arrays.equals(paramTypes, methodKey.paramTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int result = clazz.hashCode();
|
||||
result = 31 * result + methodName.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(paramTypes);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private record FieldKey(Class<?> clazz, String fieldName)
|
||||
{
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
return true;
|
||||
if(!(o instanceof FieldKey fieldKey))
|
||||
return false;
|
||||
return clazz.equals(fieldKey.clazz) && fieldName.equals(fieldKey.fieldName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int result = clazz.hashCode();
|
||||
result = 31 * result + fieldName.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private record ConstructorKey(Class<?> clazz, Class<?>[] paramTypes)
|
||||
{
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
return true;
|
||||
if(!(o instanceof ConstructorKey constructorKey))
|
||||
return false;
|
||||
return clazz.equals(constructorKey.clazz) && Arrays.equals(paramTypes, constructorKey.paramTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int result = clazz.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(paramTypes);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to chain reflection calls on the result of previous reflective operations.
|
||||
*
|
||||
@@ -373,13 +485,13 @@ public class Reflections
|
||||
if(value == null)
|
||||
return new ReflectionChain<>(null);
|
||||
|
||||
args = Wrapper.checkArgs(args);
|
||||
try
|
||||
{
|
||||
Method method = findMethod(value.getClass(), methodName, args);
|
||||
method.setAccessible(true);
|
||||
return new ReflectionChain<>((V) method.invoke(value, args));
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -399,7 +511,8 @@ public class Reflections
|
||||
{
|
||||
Field field = findField(value.getClass(), fieldName);
|
||||
return new ReflectionChain<>((V) field.get(value));
|
||||
} catch(Exception e)
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
/**
|
||||
* A serializable version of {@link BiPredicate} that can be used in contexts where the predicate needs to be serialized.
|
||||
*
|
||||
* @param <T> the type of the first argument to the predicate
|
||||
* @param <U> the type of the second argument to the predicate
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SerializableBiPredicate<T, U> extends BiPredicate<T, U>, Serializable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A functional interface that combines {@link Consumer} and {@link Serializable}.
|
||||
* <p>
|
||||
* This interface allows lambda expressions to be used as consumers while also being serializable, which is useful for scenarios where consumer logic needs to
|
||||
* be persisted or transmitted across different contexts.
|
||||
*
|
||||
* @param <T> the type of the input to the operation
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SerializableConsumer<T> extends Consumer<T>, Serializable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* A serializable version of {@link Predicate} that can be used in contexts where the predicate needs to be serialized.
|
||||
*
|
||||
* @param <T> the type of the first argument to the predicate
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SerializablePredicate<T> extends Predicate<T>, Serializable
|
||||
{
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Represents a function that accepts three arguments and produces a result.
|
||||
* This is a functional interface whose functional method is {@link #apply(Object, Object, Object)}.
|
||||
@@ -10,7 +12,7 @@ package de.eisi05.npc.api.utils;
|
||||
* @param <R> the type of the result of the function
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TriFunction<T, U, V, R>
|
||||
public interface TriFunction<T, U, V, R> extends Serializable
|
||||
{
|
||||
/**
|
||||
* Applies this function to the given arguments.
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedComponent;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntitySnapshot;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link Var} class provides utility methods for converting between Bukkit API objects
|
||||
* and their corresponding NMS (Net Minecraft Server) counterparts, as well as handling
|
||||
* component conversions and unsafe casting. These methods often rely on reflection
|
||||
* due to version-specific NMS paths.
|
||||
* The {@link Var} class provides utility methods for converting between Bukkit API objects and their corresponding NMS (Net Minecraft Server) counterparts, as
|
||||
* well as handling component conversions and unsafe casting. These methods often rely on reflection due to version-specific NMS paths.
|
||||
*/
|
||||
public class Var
|
||||
{
|
||||
/**
|
||||
* Converts a Bukkit {@link ItemStack} to its NMS (Net Minecraft Server) equivalent.
|
||||
* This method uses reflection to call the appropriate {@code asNMSCopy} method
|
||||
* from the CraftBukkit library, which varies by server version.
|
||||
* Converts a Bukkit {@link ItemStack} to its NMS (Net Minecraft Server) equivalent. This method uses reflection to call the appropriate {@code asNMSCopy}
|
||||
* method from the CraftBukkit library, which varies by server version.
|
||||
*
|
||||
* @param itemStack The Bukkit {@link ItemStack} to convert. Must not be {@code null}.
|
||||
* @return The NMS {@link Object} representing the {@code ItemStack}.
|
||||
@@ -25,14 +27,33 @@ public class Var
|
||||
*/
|
||||
public static @NotNull Object toNmsItemStack(@NotNull ItemStack itemStack)
|
||||
{
|
||||
return Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".inventory.CraftItemStack",
|
||||
"asNMSCopy", itemStack).get();
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
return Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".inventory.CraftItemStack",
|
||||
"asNMSCopy", itemStack).get();
|
||||
|
||||
return Reflections.invokeStaticMethod("org.bukkit.craftbukkit.inventory.CraftItemStack", "asNMSCopy", itemStack).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the NMS (Net Minecraft Server) equivalent of a Bukkit {@link World} object.
|
||||
* This typically involves calling the {@code getHandle()} method on the Bukkit World object
|
||||
* via reflection to access the underlying NMS world instance.
|
||||
* Converts a Bukkit {@link EntityType} to its corresponding NMS (Netty Minecraft Server) entity type object. This is a utility method that uses reflection
|
||||
* to access the internal NMS classes.
|
||||
*
|
||||
* @param entityType The Bukkit {@link EntityType} to convert
|
||||
* @return The corresponding NMS entity type object
|
||||
* @throws NullPointerException if entityType is null
|
||||
*/
|
||||
public static @NotNull Object toNmsEntityType(@NotNull EntityType entityType)
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
return Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".entity.CraftEntityType",
|
||||
"bukkitToMinecraft", entityType).get();
|
||||
return Reflections.invokeStaticMethod("org.bukkit.craftbukkit.entity.CraftEntityType",
|
||||
"bukkitToMinecraft", entityType).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the NMS (Net Minecraft Server) equivalent of a Bukkit {@link World} object. This typically involves calling the {@code getHandle()} method on
|
||||
* the Bukkit World object via reflection to access the underlying NMS world instance.
|
||||
*
|
||||
* @param world The Bukkit {@link World} to convert. Must not be {@code null}.
|
||||
* @return The NMS {@link Object} representing the {@code World} (e.g., {@code net.minecraft.server.level.WorldServer}).
|
||||
@@ -44,9 +65,8 @@ public class Var
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plain text {@link String} into a {@link WrappedComponent} (NMS chat component equivalent).
|
||||
* This method uses reflection to call the appropriate {@code fromStringOrNull} method
|
||||
* from CraftBukkit's CraftChatMessage utility, which handles parsing text into components.
|
||||
* Converts a plain text {@link String} into a {@link WrappedComponent} (NMS chat component equivalent). This method uses reflection to call the appropriate
|
||||
* {@code fromStringOrNull} method from CraftBukkit's CraftChatMessage utility, which handles parsing text into components.
|
||||
*
|
||||
* @param text The plain text {@link String} to convert. Must not be {@code null}.
|
||||
* @return A {@link WrappedComponent} representing the given text.
|
||||
@@ -54,15 +74,18 @@ public class Var
|
||||
*/
|
||||
public static @NotNull WrappedComponent fromString(@NotNull String text)
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
return WrappedComponent.fromHandle(
|
||||
Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".util.CraftChatMessage",
|
||||
"fromStringOrNull", text, true).get());
|
||||
return WrappedComponent.fromHandle(
|
||||
Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".util.CraftChatMessage",
|
||||
Reflections.invokeStaticMethod("org.bukkit.craftbukkit.util.CraftChatMessage",
|
||||
"fromStringOrNull", text, true).get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@link WrappedComponent} (NMS chat component equivalent) back into a plain text {@link String}.
|
||||
* This method uses reflection to call the appropriate {@code fromComponent} method
|
||||
* from CraftBukkit's CraftChatMessage utility.
|
||||
* Converts a {@link WrappedComponent} (NMS chat component equivalent) back into a plain text {@link String}. This method uses reflection to call the
|
||||
* appropriate {@code fromComponent} method from CraftBukkit's CraftChatMessage utility.
|
||||
*
|
||||
* @param component The {@link WrappedComponent} to convert to a string. Must not be {@code null}.
|
||||
* @return A plain text {@link String} representation of the component.
|
||||
@@ -70,15 +93,16 @@ public class Var
|
||||
*/
|
||||
public static @NotNull String toString(@NotNull WrappedComponent component)
|
||||
{
|
||||
return (String) Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".util.CraftChatMessage",
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
return (String) Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".util.CraftChatMessage",
|
||||
"fromComponent", component.getHandle()).get();
|
||||
return (String) Reflections.invokeStaticMethod("org.bukkit.craftbukkit.util.CraftChatMessage",
|
||||
"fromComponent", component.getHandle()).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an unchecked cast of an object to a specified type.
|
||||
* This method can be used to bypass Java's type checking at compile time,
|
||||
* but it comes with the risk of {@link ClassCastException} at runtime if the
|
||||
* object is not an instance of the target type.
|
||||
* Performs an unchecked cast of an object to a specified type. This method can be used to bypass Java's type checking at compile time, but it comes with
|
||||
* the risk of {@link ClassCastException} at runtime if the object is not an instance of the target type.
|
||||
*
|
||||
* @param o The object to cast. Can be {@code null}.
|
||||
* @param <T> The target type to which the object will be cast.
|
||||
@@ -89,4 +113,109 @@ public class Var
|
||||
{
|
||||
return (T) o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a WrappedEntitySnapshot.WrappedCompoundTag containing common entity state booleans into a single byte representing the entity flags for accessor
|
||||
* 0.
|
||||
* <p>
|
||||
* Each bit in the returned byte corresponds to a specific entity state:
|
||||
* <ul>
|
||||
* <li>Bit 0 (0x01) - HasVisualFire</li>
|
||||
* <li>Bit 1 (0x02) - IsCrouching</li>
|
||||
* <li>Bit 2 (0x04) - IsRiding</li>
|
||||
* <li>Bit 3 (0x08) - IsSprinting</li>
|
||||
* <li>Bit 4 (0x10) - IsSwimming</li>
|
||||
* <li>Bit 5 (0x20) - IsInvisible</li>
|
||||
* <li>Bit 6 (0x40) - IsGlowing</li>
|
||||
* <li>Bit 7 (0x80) - IsFallFlying</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param nbt The WrappedCompoundTag containing the entity state booleans.
|
||||
* @return A byte where each bit represents the corresponding entity state as listed above.
|
||||
*/
|
||||
public static byte nbtToEntityFlags(@NotNull WrappedEntitySnapshot.WrappedCompoundTag nbt)
|
||||
{
|
||||
byte flags = 0;
|
||||
|
||||
if(nbt.getBoolean("HasVisualFire"))
|
||||
flags |= 0x01;
|
||||
|
||||
if(nbt.getBoolean("IsCrouching"))
|
||||
flags |= 0x02;
|
||||
|
||||
if(nbt.getBoolean("IsRiding"))
|
||||
flags |= 0x04;
|
||||
|
||||
if(nbt.getBoolean("IsSprinting"))
|
||||
flags |= 0x08;
|
||||
|
||||
if(nbt.getBoolean("IsSwimming"))
|
||||
flags |= 0x10;
|
||||
|
||||
if(nbt.getBoolean("IsInvisible"))
|
||||
flags |= 0x20;
|
||||
|
||||
if(nbt.getBoolean("IsGlowing"))
|
||||
flags |= 0x40;
|
||||
|
||||
if(nbt.getBoolean("IsFallFlying"))
|
||||
flags |= (byte) 0x80;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the entity flag byte (accessor 0) from the current state of a Bukkit {@link Entity}.
|
||||
* <p>
|
||||
* This method interprets Bukkit-level entity state as user intent and converts it into the corresponding Minecraft entity flags. It is intended to be used
|
||||
* as an internal translation layer when NMS classes must not be exposed to API consumers.
|
||||
* <p>
|
||||
* <b>Important:</b> This is a best-effort mapping. Bukkit state is not a perfect mirror
|
||||
* of the underlying NMS entity flags, so some flags (such as visual fire or swimming) are approximated based on available Bukkit APIs.
|
||||
*
|
||||
* <p>Flag mapping:</p>
|
||||
* <ul>
|
||||
* <li>Bit 0 (0x01) - Visual fire (fire ticks or visual fire)</li>
|
||||
* <li>Bit 1 (0x02) - Sneaking / crouching</li>
|
||||
* <li>Bit 2 (0x04) - Riding / inside vehicle</li>
|
||||
* <li>Bit 3 (0x08) - Sprinting</li>
|
||||
* <li>Bit 4 (0x10) - Swimming</li>
|
||||
* <li>Bit 5 (0x20) - Invisible</li>
|
||||
* <li>Bit 6 (0x40) - Glowing</li>
|
||||
* <li>Bit 7 (0x80) - Fall flying (elytra gliding)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param entity The Bukkit entity whose state should be converted into entity flags.
|
||||
* @return A byte representing the combined entity flags for accessor 0.
|
||||
*/
|
||||
public static byte extractFlagsFromBukkit(@NotNull Entity entity)
|
||||
{
|
||||
byte flags = 0;
|
||||
|
||||
if(entity.getFireTicks() > 0 || entity.isVisualFire())
|
||||
flags |= 0x01;
|
||||
|
||||
if(entity instanceof Player player && player.isSneaking())
|
||||
flags |= 0x02;
|
||||
|
||||
if(entity.isInsideVehicle())
|
||||
flags |= 0x04;
|
||||
|
||||
if(entity instanceof Player player && player.isSprinting())
|
||||
flags |= 0x08;
|
||||
|
||||
if(entity instanceof LivingEntity le && le.isSwimming())
|
||||
flags |= 0x10;
|
||||
|
||||
if(entity instanceof LivingEntity le && le.isInvisible())
|
||||
flags |= 0x20;
|
||||
|
||||
if(entity.isGlowing())
|
||||
flags |= 0x40;
|
||||
|
||||
if(entity instanceof LivingEntity le && le.isGliding())
|
||||
flags |= (byte) 0x80;
|
||||
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,15 @@ public enum Versions
|
||||
/**
|
||||
* Minecraft 1.21.9 version.
|
||||
*/
|
||||
V1_21_9("v1_21_R6");
|
||||
V1_21_9("v1_21_R6"),
|
||||
/**
|
||||
* Minecraft 1.21.11 version.
|
||||
*/
|
||||
V1_21_11("v1_21_R7"),
|
||||
/**
|
||||
* Minecraft 26.1 version.
|
||||
*/
|
||||
V26_1("v26_1");
|
||||
|
||||
/**
|
||||
* Caches the determined current server version to avoid repeated lookups.
|
||||
@@ -122,6 +130,9 @@ public enum Versions
|
||||
if(VERSION != null)
|
||||
return VERSION;
|
||||
|
||||
if(Bukkit.getServer().getBukkitVersion().startsWith("26.1"))
|
||||
return V26_1;
|
||||
|
||||
String nmsVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
|
||||
|
||||
return VERSION = Arrays.stream(Versions.values())
|
||||
|
||||
@@ -260,7 +260,21 @@ public abstract class Wrapper implements HandleHolder
|
||||
String callingMethodName = CallerUtils.getCallerMethodName();
|
||||
Class<?> callerClass = CallerUtils.getCallerClass();
|
||||
|
||||
Class<?> targetClass = getTargetClass(callerClass.getAnnotation(Mapping.class));
|
||||
Mapping[] classAnnotations = callerClass.getAnnotationsByType(Mapping.class);
|
||||
if(classAnnotations.length == 0)
|
||||
throw new IllegalStateException("Missing @WrapData on class: " + callerClass.getName());
|
||||
|
||||
Class<?> targetClass = null;
|
||||
for(Mapping classData : classAnnotations)
|
||||
{
|
||||
if(!Versions.containsCurrentVersion(classData))
|
||||
continue;
|
||||
|
||||
targetClass = getTargetClass(classData);
|
||||
}
|
||||
|
||||
if(targetClass == null)
|
||||
throw new VersionNotFound(callerClass.getName() + " -> " + callingMethodName);
|
||||
|
||||
Method callingMethod = null;
|
||||
Class<?> clazz = callerClass;
|
||||
@@ -290,11 +304,12 @@ public abstract class Wrapper implements HandleHolder
|
||||
Arrays.toString(Arrays.stream(newArgs)
|
||||
.map(a -> a == null ? "null" : a.getClass().getName()).toArray());
|
||||
|
||||
Class<?> finalTargetClass = targetClass;
|
||||
MethodHandle handle = methodCache.computeIfAbsent(key, k ->
|
||||
{
|
||||
try
|
||||
{
|
||||
Method method = Arrays.stream(targetClass.getMethods())
|
||||
Method method = Arrays.stream(finalTargetClass.getMethods())
|
||||
.filter(m -> m.getName().equals(methodPath))
|
||||
.filter(m ->
|
||||
{
|
||||
@@ -409,7 +424,7 @@ public abstract class Wrapper implements HandleHolder
|
||||
return true;
|
||||
})
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException(new NoSuchMethodException(
|
||||
.orElseThrow(() -> new RuntimeException(new NoSuchMethodException(getHandle().getClass() + " -> " +
|
||||
methodPath + "(" + Arrays.stream(newArgs).map(o -> o.getClass().toString()).toList() + ")")));
|
||||
|
||||
method.setAccessible(true);
|
||||
|
||||
@@ -9,73 +9,96 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.EnumChatFormat")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.ChatFormatting")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.EnumChatFormat")
|
||||
public enum ChatFormat implements Wrapper.EnumWrapper, Serializable
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BLACK")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
BLACK(ChatColor.BLACK),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DARK_BLUE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
DARK_BLUE(ChatColor.DARK_BLUE),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DARK_GREEN")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
DARK_GREEN(ChatColor.DARK_GREEN),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DARK_AQUA")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
DARK_AQUA(ChatColor.DARK_AQUA),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DARK_RED")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "e")
|
||||
DARK_RED(ChatColor.DARK_RED),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DARK_PURPLE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "f")
|
||||
DARK_PURPLE(ChatColor.DARK_PURPLE),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "GOLD")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "g")
|
||||
GOLD(ChatColor.GOLD),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "GRAY")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "h")
|
||||
GRAY(ChatColor.GRAY),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "i")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DARK_GRAY")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "i")
|
||||
DARK_GRAY(ChatColor.DARK_GRAY),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "j")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BLUE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "j")
|
||||
BLUE(ChatColor.BLUE),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "GREEN")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "k")
|
||||
GREEN(ChatColor.GREEN),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "l")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "AQUA")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "l")
|
||||
AQUA(ChatColor.AQUA),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "m")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "RED")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "m")
|
||||
RED(ChatColor.RED),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "LIGHT_PURPLE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "n")
|
||||
LIGHT_PURPLE(ChatColor.LIGHT_PURPLE),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "o")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "YELLOW")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "o")
|
||||
YELLOW(ChatColor.YELLOW),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "p")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "WHITE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "p")
|
||||
WHITE(ChatColor.WHITE),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "q")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OBFUSCATED")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "q")
|
||||
OBFUSCATED(ChatColor.MAGIC),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "r")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BOLD")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "r")
|
||||
BOLD(ChatColor.BOLD),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "s")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "STRIKETHROUGH")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "s")
|
||||
STRIKETHROUGH(ChatColor.STRIKETHROUGH),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "t")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "UNDERLINE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "t")
|
||||
UNDERLINE(ChatColor.UNDERLINE),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "u")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ITALIC")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "u")
|
||||
ITALIC(ChatColor.ITALIC),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "v")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "RESET")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "v")
|
||||
RESET(ChatColor.RESET);
|
||||
|
||||
private final ChatColor color;
|
||||
|
||||
@@ -7,25 +7,31 @@ import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedConnection;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.EnumProtocol")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.ConnectionProtocol")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.EnumProtocol")
|
||||
public enum ConnectionProtocol implements Wrapper.EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "HANDSHAKE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
HANDSHAKE,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "PLAY")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
PLAY,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "STATUS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
STATUS,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "LOGIN")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
LOGIN,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "CONFIGURATION")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "e")
|
||||
CONFIGURATION;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
public @NotNull Object codecData(@NotNull WrappedConnection.WrappedNetworkManager.PacketFlow packetFlow)
|
||||
{
|
||||
return Reflections.invokeMethod(getHandle(), getPath(), packetFlow.getHandle()).get();
|
||||
|
||||
@@ -5,31 +5,40 @@ import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.entity.EnumItemSlot")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.entity.EquipmentSlot")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.entity.EnumItemSlot")
|
||||
public enum EquipmentSlot implements Wrapper.EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "MAINHAND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
MAINHAND,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OFFHAND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
OFFHAND,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "FEET")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
FEET,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "LEGS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
LEGS,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "CHEST")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "e")
|
||||
CHEST,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "HEAD")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "f")
|
||||
HEAD,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BODY")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "g")
|
||||
BODY,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SADDLE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "h")
|
||||
SADDLE;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,13 +5,16 @@ import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.EnumHand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.InteractionHand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.EnumHand")
|
||||
public enum InteractionHand implements Wrapper.EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "MAIN_HAND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
MAIN_HAND,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OFF_HAND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
OFF_HAND;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,65 +9,84 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.entity.EntityPose")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.entity.Pose")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.entity.EntityPose")
|
||||
public enum Pose implements Wrapper.EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "STANDING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
STANDING(getBukkit("STANDING"), Material.ARMOR_STAND),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "FALL_FLYING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
FALL_FLYING(getBukkit("FALL_FLYING"), Material.FEATHER),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SLEEPING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
SLEEPING(getBukkit("SLEEPING"), Material.RED_BED),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SWIMMING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
SWIMMING(getBukkit("SWIMMING"), Material.WATER_BUCKET),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SPIN_ATTACK")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "e")
|
||||
SPIN_ATTACK(getBukkit("SPIN_ATTACK"), Material.TRIDENT),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "CROUCHING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "f")
|
||||
CROUCHING(getBukkit("SNEAKING"), Material.LEATHER_BOOTS),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "LONG_JUMPING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "g")
|
||||
LONG_JUMPING(getBukkit("LONG_JUMPING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "h")
|
||||
DYING(getBukkit("DYING"), Material.ROTTEN_FLESH),
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DYING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "h")
|
||||
DYING(getBukkit("DYING"), Versions.isCurrentVersionSmallerThan(Versions.V1_19_4) ? Material.ROTTEN_FLESH : null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "i")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "CROAKING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_11), path = "i")
|
||||
CROAKING(getBukkit("CROAKING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "j")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "USING_TONGUE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_11), path = "j")
|
||||
USING_TONGUE(getBukkit("USING_TONGUE"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "k")
|
||||
SITTING(getBukkit("SITTING"), null),
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SITTING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "k")
|
||||
SITTING(getBukkit("SITTING"), Versions.isCurrentVersionSmallerThan(Versions.V1_19_4) ? null : Material.OAK_STAIRS),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "l")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ROARING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "l")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "k")
|
||||
ROARING(getBukkit("ROARING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "m")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SNIFFING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "m")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "l")
|
||||
SNIFFING(getBukkit("SNIFFING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "EMERGING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "m")
|
||||
EMERGING(getBukkit("EMERGING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "o")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DIGGING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "o")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "n")
|
||||
DIGGING(getBukkit("DIGGING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_9), path = "p")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SLIDING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_11), path = "p")
|
||||
SLIDING(getBukkit("SLIDING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_9), path = "q")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SHOOTING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_11), path = "q")
|
||||
SHOOTING(getBukkit("SHOOTING"), null),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_9), path = "r")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "INHALING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_11), path = "r")
|
||||
INHALING(getBukkit("INHALING"), null);
|
||||
|
||||
private final org.bukkit.entity.Pose bukkitPose;
|
||||
|
||||
@@ -8,7 +8,8 @@ import org.bukkit.entity.ArmorStand;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.entity.decoration.EntityArmorStand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.entity.decoration.ArmorStand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.entity.decoration.EntityArmorStand")
|
||||
public class WrappedArmorStand extends WrappedEntity.WrappedNameTag<ArmorStand>
|
||||
{
|
||||
private WrappedArmorStand(Object handle)
|
||||
|
||||
@@ -6,8 +6,8 @@ import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.world.entity.ai.attributes.AttributeModifiable")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.entity.ai.attributes.AttributeInstance")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "net.minecraft.world.entity.ai.attributes.AttributeModifiable")
|
||||
public class WrappedAttributeInstance extends Wrapper
|
||||
{
|
||||
WrappedAttributeInstance(Object handle)
|
||||
@@ -20,16 +20,19 @@ public class WrappedAttributeInstance extends Wrapper
|
||||
super(createInstance(WrappedAttributeInstance.class, holder, consumer));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setBaseValue")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "a")
|
||||
public void setBaseValue(double value)
|
||||
{
|
||||
invokeWrappedMethod(value);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.world.entity.ai.attributes.GenericAttributes")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.entity.ai.attributes.Attributes")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "net.minecraft.world.entity.ai.attributes.GenericAttributes")
|
||||
public static class Attributes extends Wrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SCALE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V1_21_11), path = "A")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "z")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_5), path = "y")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_6), path = "t")
|
||||
|
||||
@@ -5,7 +5,7 @@ import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "net.minecraft.server.level.ClientInformation")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V26_1), path = "net.minecraft.server.level.ClientInformation")
|
||||
public class WrappedClientInformation extends Wrapper
|
||||
{
|
||||
private WrappedClientInformation(Object handle)
|
||||
@@ -13,7 +13,8 @@ public class WrappedClientInformation extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "createDefault")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_11), path = "a")
|
||||
public static @NotNull WrappedClientInformation createDefault()
|
||||
{
|
||||
return new WrappedClientInformation(invokeStaticWrappedMethod());
|
||||
|
||||
@@ -6,7 +6,8 @@ import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.commands.CommandListenerWrapper")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.commands.CommandSourceStack")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.commands.CommandListenerWrapper")
|
||||
public class WrappedCommandSourceStack extends Wrapper
|
||||
{
|
||||
public WrappedCommandSourceStack(Object handle)
|
||||
@@ -14,21 +15,43 @@ public class WrappedCommandSourceStack extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "getBukkitSender")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "getBukkitSender")
|
||||
public CommandSender getBukkitSender()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "withMaximumPermission")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V1_21_11), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
public @NotNull WrappedCommandSourceStack withMaximumPermission()
|
||||
{
|
||||
return new WrappedCommandSourceStack(invokeWrappedMethod(4));
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_11))
|
||||
return new WrappedCommandSourceStack(invokeWrappedMethod(4));
|
||||
return new WrappedCommandSourceStack(invokeWrappedMethod(WrappedPermissionSet.ALL.getHandle()));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "withSuppressedOutput")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
public @NotNull WrappedCommandSourceStack withSuppressedOutput()
|
||||
{
|
||||
return new WrappedCommandSourceStack(invokeWrappedMethod());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V26_1), path = "net.minecraft.server.permissions.PermissionSet")
|
||||
private static class WrappedPermissionSet extends Wrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "NO_PERMISSIONS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V1_21_11), path = "g")
|
||||
public static final WrappedPermissionSet NO = new WrappedPermissionSet(getStaticWrappedFieldValue("NO").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ALL_PERMISSIONS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V1_21_11), path = "h")
|
||||
public static final WrappedPermissionSet ALL = new WrappedPermissionSet(getStaticWrappedFieldValue("ALL").orElse(null));
|
||||
|
||||
private WrappedPermissionSet(Object handle)
|
||||
{
|
||||
super(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,25 +21,54 @@ import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.chat.IChatBaseComponent")
|
||||
/**
|
||||
* A wrapper class for Minecraft's chat component system, providing a type-safe way to create and manipulate
|
||||
* chat messages with formatting, click events, hover events, and other rich text features.
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.Component")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.chat.IChatBaseComponent")
|
||||
public class WrappedComponent extends Wrapper
|
||||
{
|
||||
/**
|
||||
* Creates a new WrappedComponent instance from a native Minecraft component.
|
||||
*
|
||||
* @param handle The native Minecraft component object
|
||||
*/
|
||||
private WrappedComponent(Object handle)
|
||||
{
|
||||
super(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WrappedComponent from a native Minecraft component handle.
|
||||
*
|
||||
* @param handle The native Minecraft component object (can be null)
|
||||
* @return A new WrappedComponent instance, or null if handle is null
|
||||
*/
|
||||
public static WrappedComponent fromHandle(@Nullable Object handle)
|
||||
{
|
||||
return new WrappedComponent(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
/**
|
||||
* Creates a new text component with the specified text.
|
||||
*
|
||||
* @param text The text to display (can be null for empty component)
|
||||
* @return A new WrappedComponent containing the specified text
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "literal")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
public static @NotNull WrappedComponent create(@Nullable String text)
|
||||
{
|
||||
return new WrappedComponent(text == null ? CommonComponents.EMPTY : invokeStaticWrappedMethod(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a legacy formatted string (using § color codes) into a WrappedComponent.
|
||||
*
|
||||
* @param legacy The legacy formatted string (e.g., "§aHello &bWorld!")
|
||||
* @return A new WrappedComponent with the parsed formatting
|
||||
*/
|
||||
public static @NotNull WrappedComponent parseFromLegacy(@Nullable String legacy)
|
||||
{
|
||||
if(legacy == null || legacy.isEmpty())
|
||||
@@ -48,7 +77,14 @@ public class WrappedComponent extends Wrapper
|
||||
return Var.fromString(legacy);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
/**
|
||||
* Applies the specified chat formats to this component.
|
||||
*
|
||||
* @param format One or more ChatFormat values to apply (e.g., BOLD, ITALIC, UNDERLINE)
|
||||
* @return This component for method chaining
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "withStyle")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
public @NotNull WrappedComponent setFormats(@NotNull ChatFormat... format)
|
||||
{
|
||||
if(format.length == 0)
|
||||
@@ -77,7 +113,14 @@ public class WrappedComponent extends Wrapper
|
||||
return this;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
/**
|
||||
* Adds a click event to this component.
|
||||
*
|
||||
* @param clickEvent The click event to add
|
||||
* @return This component for method chaining
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "withStyle")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "format")
|
||||
public @NotNull WrappedComponent withClickEvent(@NotNull ClickEvent clickEvent)
|
||||
{
|
||||
@@ -85,7 +128,14 @@ public class WrappedComponent extends Wrapper
|
||||
return this;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
/**
|
||||
* Adds a hover event to this component.
|
||||
*
|
||||
* @param hoverEvent The hover event to add
|
||||
* @return This component for method chaining
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "withStyle")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "format")
|
||||
public @NotNull WrappedComponent withHoverEvent(@NotNull HoverEvent hoverEvent)
|
||||
{
|
||||
@@ -93,7 +143,14 @@ public class WrappedComponent extends Wrapper
|
||||
return this;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_1, to = Versions.V1_21_9), path = "b")
|
||||
/**
|
||||
* Appends another component to this component.
|
||||
*
|
||||
* @param wrappedComponent The component to append
|
||||
* @return This component for method chaining
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "append")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_1, to = Versions.V1_21_11), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_19), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "addSibling")
|
||||
public @NotNull WrappedComponent append(WrappedComponent wrappedComponent)
|
||||
@@ -102,7 +159,13 @@ public class WrappedComponent extends Wrapper
|
||||
return this;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_9), path = "f")
|
||||
/**
|
||||
* Creates a deep copy of this component.
|
||||
*
|
||||
* @return A new WrappedComponent that is a copy of this one
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "copy")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_11), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_20_2), path = "e")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "mutableCopy")
|
||||
public @NotNull WrappedComponent copy()
|
||||
@@ -110,26 +173,44 @@ public class WrappedComponent extends Wrapper
|
||||
return new WrappedComponent(invokeWrappedMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this component to a legacy formatted string.
|
||||
*
|
||||
* @param withLineBreak Whether to preserve newline characters
|
||||
* @return A legacy formatted string with color codes
|
||||
*/
|
||||
public @NotNull String toLegacy(boolean withLineBreak)
|
||||
{
|
||||
return withLineBreak ? Var.toString(this) : Var.toString(this).replace("\n", "\\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this component to a plain text string without color codes.
|
||||
*
|
||||
* @param withLineBreak Whether to preserve newline characters
|
||||
* @return A plain text string without formatting
|
||||
*/
|
||||
public @NotNull String toLegacyNoColor(boolean withLineBreak)
|
||||
{
|
||||
return ChatColor.stripColor(toLegacy(withLineBreak));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this component to a SerializedComponent for storage.
|
||||
*
|
||||
* @return A serializable representation of this component
|
||||
*/
|
||||
public @NotNull SerializedComponent serialize()
|
||||
{
|
||||
return new SerializedComponent(toLegacy(true));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "net.minecraft.network.chat.CommonComponents")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V26_1), path = "net.minecraft.network.chat.CommonComponents")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "net.minecraft.network.chat.ChatComponentText")
|
||||
private static class CommonComponents extends Wrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "EMPTY")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "d")
|
||||
private static final Object EMPTY = getStaticWrappedFieldValue("EMPTY").orElse(null);
|
||||
|
||||
@@ -139,7 +220,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.chat.ChatClickable")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable")
|
||||
public static class ClickEvent extends Wrapper
|
||||
{
|
||||
private ClickEvent(Object handle)
|
||||
@@ -154,34 +236,42 @@ public class WrappedComponent extends Wrapper
|
||||
return action.function.apply(value);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$EnumClickAction")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$Action")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$EnumClickAction")
|
||||
public enum ClickAction implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPEN_URL")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
OPEN_URL(o -> new OpenUrl((URI) o)),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPEN_FILE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
OPEN_FILE(o -> new OpenFile((String) o)),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "RUN_COMMAND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
RUN_COMMAND(o -> new RunCommand((String) o)),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SUGGEST_COMMAND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
SUGGEST_COMMAND(o -> new SuggestCommand((String) o)),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SHOW_DIALOG")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "e")
|
||||
SHOW_DIALOG(ShowDialog::new),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "CHANGE_PAGE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "e")
|
||||
CHANGE_PAGE(o -> new ChangePage((int) o)),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "COPY_TO_CLIPBOARD")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "f")
|
||||
COPY_TO_CLIPBOARD(o -> new CopyToClipboard((String) o)),
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "CUSTOM")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "h")
|
||||
CUSTOM(o -> new Custom(((Object[]) o)[0], (Optional<?>) ((Object[]) o)[1]));
|
||||
|
||||
private final Function<Object, ClickEvent> function;
|
||||
@@ -198,8 +288,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$OpenUrl")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$OpenUrl")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$OpenUrl")
|
||||
private static class OpenUrl extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private OpenUrl(URI url)
|
||||
@@ -208,8 +298,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$OpenFile")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$OpenFile")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$OpenFile")
|
||||
private static class OpenFile extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private OpenFile(String file)
|
||||
@@ -218,8 +308,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$RunCommand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$RunCommand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$RunCommand")
|
||||
private static class RunCommand extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private RunCommand(String command)
|
||||
@@ -228,8 +318,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$SuggestCommand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$SuggestCommand")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$SuggestCommand")
|
||||
private static class SuggestCommand extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private SuggestCommand(String command)
|
||||
@@ -238,8 +328,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$ChangePage")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$ChangePage")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$ChangePage")
|
||||
private static class ChangePage extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private ChangePage(int page)
|
||||
@@ -248,8 +338,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$CopyToClipboard")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$CopyToClipboard")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$CopyToClipboard")
|
||||
private static class CopyToClipboard extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private CopyToClipboard(String string)
|
||||
@@ -258,8 +348,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$ShowDialog")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$h")
|
||||
private static class ShowDialog extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private ShowDialog(Object dialogHolder)
|
||||
@@ -268,8 +358,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatClickable$d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.ClickEvent$Custom")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatClickable$d")
|
||||
private static class Custom extends WrappedComponent.ClickEvent
|
||||
{
|
||||
private Custom(Object key, Optional<?> optionalNbtBase)
|
||||
@@ -279,7 +369,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.chat.ChatHoverable")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.HoverEvent")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatHoverable")
|
||||
public static class HoverEvent extends Wrapper
|
||||
{
|
||||
private HoverEvent(Object handle)
|
||||
@@ -294,7 +385,8 @@ public class WrappedComponent extends Wrapper
|
||||
return new ShowText(component);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9), path = "net.minecraft.network.chat.ChatHoverable$e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.HoverEvent$ShowText")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatHoverable$e")
|
||||
private static class ShowText extends HoverEvent
|
||||
{
|
||||
private ShowText(@NotNull WrappedComponent component)
|
||||
@@ -303,11 +395,12 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.chat.ChatHoverable$EnumHoverAction")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.HoverEvent$Action")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatHoverable$EnumHoverAction")
|
||||
private static class HoverAction extends Wrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SHOW_TEXT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
private static final Object SHOW_TEXT = getStaticWrappedFieldValue("SHOW_TEXT").orElse(null);
|
||||
|
||||
private HoverAction()
|
||||
@@ -335,7 +428,8 @@ public class WrappedComponent extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.chat.ChatModifier")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.chat.Style")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.chat.ChatModifier")
|
||||
public static class Style extends Wrapper
|
||||
{
|
||||
private Style(Object handle)
|
||||
@@ -343,14 +437,16 @@ public class WrappedComponent extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "withClickEvent")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setChatClickable")
|
||||
public @NotNull Style withClickEvent(@NotNull ClickEvent clickEvent)
|
||||
{
|
||||
return new Style(invokeWrappedMethod(clickEvent));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "withHoverEvent")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setChatHoverable")
|
||||
public @NotNull Style withHoverEvent(@NotNull HoverEvent hoverEvent)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,8 @@ import io.netty.util.AttributeKey;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "net.minecraft.server.network.PlayerConnection")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.server.network.ServerGamePacketListenerImpl")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "net.minecraft.server.network.PlayerConnection")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "net.minecraft.network.PlayerConnection")
|
||||
public class WrappedConnection extends Wrapper
|
||||
{
|
||||
@@ -27,7 +28,8 @@ public class WrappedConnection extends Wrapper
|
||||
super(createInstance(WrappedConnection.class, server, networkManager, player, commonListenerCookie));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "sendPacket")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_11), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_20), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "sendPacket")
|
||||
public void sendPacket(@NotNull PacketWrapper packet)
|
||||
@@ -35,7 +37,8 @@ public class WrappedConnection extends Wrapper
|
||||
invokeWrappedMethod(packet);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "connection")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_20_4), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_3), path = "b")
|
||||
@@ -45,7 +48,8 @@ public class WrappedConnection extends Wrapper
|
||||
return new WrappedNetworkManager(getWrappedFieldValue());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.NetworkManager")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.Connection")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.NetworkManager")
|
||||
public static class WrappedNetworkManager extends Wrapper
|
||||
{
|
||||
private WrappedNetworkManager(Object handle)
|
||||
@@ -69,6 +73,8 @@ public class WrappedConnection extends Wrapper
|
||||
return networkManager;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "channel")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V1_21_11), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18_2, to = Versions.V1_20), path = "m")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18), path = "k")
|
||||
@@ -77,6 +83,8 @@ public class WrappedConnection extends Wrapper
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "channel")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V1_21_11), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18_2, to = Versions.V1_20), path = "m")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18), path = "k")
|
||||
@@ -85,14 +93,16 @@ public class WrappedConnection extends Wrapper
|
||||
setWrappedFieldValue(channel);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9),
|
||||
path = "net.minecraft.network.protocol.EnumProtocolDirection")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.PacketFlow")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.EnumProtocolDirection")
|
||||
public enum PacketFlow implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "SERVERBOUND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_11), path = "a")
|
||||
SERVERBOUND,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "CLIENTBOUND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_11), path = "b")
|
||||
CLIENTBOUND;
|
||||
|
||||
@Override
|
||||
@@ -103,7 +113,7 @@ public class WrappedConnection extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "net.minecraft.server.network.CommonListenerCookie")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V26_1), path = "net.minecraft.server.network.CommonListenerCookie")
|
||||
public static class CommonListenerCookie extends Wrapper
|
||||
{
|
||||
public CommonListenerCookie(@NotNull GameProfile profile, int latency, boolean transferred)
|
||||
|
||||
@@ -16,12 +16,16 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.entity.Entity")
|
||||
public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.entity.Entity")
|
||||
public class WrappedEntity<T extends Entity> extends Wrapper
|
||||
{
|
||||
protected WrappedEntity(Object handle)
|
||||
public String data = "{}";
|
||||
private final BoundingBox defaultBoundingBox;
|
||||
|
||||
WrappedEntity(Object handle)
|
||||
{
|
||||
super(handle);
|
||||
defaultBoundingBox = getBoundingBox();
|
||||
}
|
||||
|
||||
public static <T extends Entity, V extends WrappedEntity<T>> @NotNull V fromEntity(@NotNull T entity, @NotNull Class<V> clazz)
|
||||
@@ -31,7 +35,8 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
Constructor<V> constructor = clazz.getDeclaredConstructor(Object.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(Reflections.invokeMethod(entity, "getHandle").get());
|
||||
} catch(NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e)
|
||||
}
|
||||
catch(NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -52,21 +57,23 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
return false;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "getBukkitEntity")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "getBukkitEntity")
|
||||
public @NotNull T getBukkitPlayer()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "absSnapTo")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setLocation")
|
||||
public void moveTo(@NotNull Location location)
|
||||
{
|
||||
invokeWrappedMethod(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
|
||||
}
|
||||
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "aC")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getEntityData")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "aD")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "aC")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "au")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_5), path = "ar")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "au")
|
||||
@@ -83,7 +90,8 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
return new WrappedEntityData(invokeWrappedMethod());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "aS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "passengers")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "aS")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "aR")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_5), path = "u")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "q")
|
||||
@@ -91,12 +99,14 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "r")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_19_3), path = "au")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "at")
|
||||
protected void setPassengers(WrappedEntity<?>... entities)
|
||||
public void setPassengers(WrappedEntity<?>... entities)
|
||||
{
|
||||
setWrappedFieldValue(ImmutableList.copyOf(Arrays.stream(entities).map(wrappedEntity -> wrappedEntity.handle).toList()));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "an")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "level")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "ao")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "an")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "ai")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "cU")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21), path = "cN")
|
||||
@@ -114,7 +124,8 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getAddEntityPacket")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_6), path = "dl")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "dj")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "di")
|
||||
@@ -129,7 +140,9 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
return new CustomPacket(invokeWrappedMethod(new WrappedEntityTrackerEntry(this)));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "de")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getBoundingBox")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "dj")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "de")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "cV")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "cR")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21), path = "cK")
|
||||
@@ -145,6 +158,18 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
return new BoundingBox(invokeWrappedMethod());
|
||||
}
|
||||
|
||||
public @NotNull BoundingBox getDefaultBoundingBox()
|
||||
{
|
||||
return defaultBoundingBox;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setId")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "e")
|
||||
public void setId(int id)
|
||||
{
|
||||
invokeWrappedMethod(id);
|
||||
}
|
||||
|
||||
public int getId()
|
||||
{
|
||||
return getBukkitPlayer().getEntityId();
|
||||
@@ -155,17 +180,62 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
return getBukkitPlayer().getWorld();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.entity.EntityTypes")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V26_1), path = "net.minecraft.world.entity.Entity$RemovalReason")
|
||||
enum RemovalReason implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DISCARDED")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_11), path = "b")
|
||||
DISCARDED;
|
||||
|
||||
@Override
|
||||
public @NotNull Object getHandle()
|
||||
{
|
||||
return cast(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V26_1), path = "net.minecraft.world.entity.EntitySpawnReason")
|
||||
enum SpawnReason implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "LOAD")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_11), path = "r")
|
||||
LOAD;
|
||||
|
||||
@Override
|
||||
public @NotNull Object getHandle()
|
||||
{
|
||||
return cast(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V26_1), path = "net.minecraft.world.entity.EntityProcessor")
|
||||
static class EntityProcessor extends Wrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "NOP")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "a")
|
||||
public static final Object NOP = getStaticWrappedFieldValue("NOP").orElse(null);
|
||||
|
||||
private EntityProcessor()
|
||||
{
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.entity.EntityType")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.entity.EntityTypes")
|
||||
public static class EntityTypes extends Wrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ARMOR_STAND")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "c")
|
||||
public static final Object ARMOR_STAND = getStaticWrappedFieldValue("ARMOR_STAND").orElse(null);
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "bA")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "TEXT_DISPLAY")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "bD")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "bA")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "bx")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_5), path = "bw")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_4), path = "bu")
|
||||
@@ -181,7 +251,8 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.phys.AxisAlignedBB")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.phys.AABB")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.phys.AxisAlignedBB")
|
||||
public static class BoundingBox extends Wrapper
|
||||
{
|
||||
private BoundingBox(Object handle)
|
||||
@@ -189,11 +260,26 @@ public abstract class WrappedEntity<T extends Entity> extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getXsize")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
public double getXSize()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getYsize")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
public double getYSize()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getZsize")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
public double getZSize()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class WrappedNameTag<T extends Entity> extends WrappedEntity<T>
|
||||
|
||||
@@ -13,7 +13,8 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.syncher.DataWatcher")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.syncher.SynchedEntityData")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.syncher.DataWatcher")
|
||||
public class WrappedEntityData extends Wrapper
|
||||
{
|
||||
WrappedEntityData(Object handle)
|
||||
@@ -21,28 +22,47 @@ public class WrappedEntityData extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "set")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_20_4), path = "b")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "set")
|
||||
public <T> void set(@NotNull EntityDataAccessor<T> accessor, @Nullable T value)
|
||||
{
|
||||
invokeWrappedMethod(accessor, value);
|
||||
markDirty(accessor);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "get")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "get")
|
||||
public <T> @Nullable T get(@NotNull EntityDataAccessor<T> accessor)
|
||||
{
|
||||
return invokeWrappedMethod(accessor);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "packDirty")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
public @NotNull List<?> packDirty()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "markDirty")
|
||||
public <T> void markDirty(@NotNull EntityDataAccessor<T> accessor)
|
||||
{
|
||||
invokeWrappedMethod(accessor);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getNonDefaultValues")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "c")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getAll")
|
||||
public List<?> getAll()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.syncher.DataWatcherObject")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.syncher.EntityDataAccessor")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.syncher.DataWatcherObject")
|
||||
public static class EntityDataAccessor<T> extends Wrapper
|
||||
{
|
||||
private EntityDataAccessor(Object handle)
|
||||
@@ -56,7 +76,8 @@ public class WrappedEntityData extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.syncher.DataWatcherSerializer")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.syncher.EntityDataSerializer")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.syncher.DataWatcherSerializer")
|
||||
public static class EntityDataSerializer<T> extends Wrapper
|
||||
{
|
||||
private EntityDataSerializer(Object handle)
|
||||
@@ -70,86 +91,104 @@ public class WrappedEntityData extends Wrapper
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.syncher.DataWatcherRegistry")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.syncher.EntityDataSerializers")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.syncher.DataWatcherRegistry")
|
||||
public static class EntityDataSerializers extends Wrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BYTE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
public static final EntityDataSerializer<Byte> BYTE = new EntityDataSerializer<>(getStaticWrappedFieldValue("BYTE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "INT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
public static final EntityDataSerializer<Integer> INT = new EntityDataSerializer<>(getStaticWrappedFieldValue("INT").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "LONG")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "c")
|
||||
public static final EntityDataSerializer<Long> LONG = new EntityDataSerializer<>(getStaticWrappedFieldValue("LONG").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "FLOAT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "c")
|
||||
public static final EntityDataSerializer<Float> FLOAT = new EntityDataSerializer<>(getStaticWrappedFieldValue("FLOAT").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "STRING")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "d")
|
||||
public static final EntityDataSerializer<String> STRING = new EntityDataSerializer<>(getStaticWrappedFieldValue("STRING").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "COMPONENT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "e")
|
||||
public static final EntityDataSerializer<?> CHAT_COMPONENT = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("CHAT_COMPONENT").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPTIONAL_COMPONENT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "f")
|
||||
public static final EntityDataSerializer<Optional<?>> OPTIONAL_CHAT_COMPONENT = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("OPTIONAL_CHAT_COMPONENT").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ITEM_STACK")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "g")
|
||||
public static final EntityDataSerializer<?> ITEM_STACK = new EntityDataSerializer<>(getStaticWrappedFieldValue("ITEM_STACK").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_9), path = "i")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BLOCK_STATE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_11), path = "i")
|
||||
public static final EntityDataSerializer<?> BLOCK_DATA = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("BLOCK_DATA").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_9), path = "j")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPTIONAL_BLOCK_STATE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_11), path = "j")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "i")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "h")
|
||||
public static final EntityDataSerializer<Optional<?>> OPTIONAL_BLOCK_DATA = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("OPTIONAL_BLOCK_DATA").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_9), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BOOLEAN")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_11), path = "k")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "j")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "i")
|
||||
public static final EntityDataSerializer<Boolean> BOOLEAN = new EntityDataSerializer<>(getStaticWrappedFieldValue("BOOLEAN").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_9), path = "l")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "PARTICLE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_11), path = "l")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "j")
|
||||
public static final EntityDataSerializer<?> PARTICLE_PARAM = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("PARTICLE_PARAM").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "m")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "PARTICLES")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "m")
|
||||
public static final EntityDataSerializer<List<?>> PARTICLE_PARAM_LIST = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("PARTICLE_PARAM_LIST").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ROTATIONS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "m")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "l")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "k")
|
||||
public static final EntityDataSerializer<?> VECTOR = new EntityDataSerializer<>(getStaticWrappedFieldValue("VECTOR").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "o")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "BLOCK_POS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "o")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "n")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "m")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "l")
|
||||
public static final EntityDataSerializer<?> BLOCK_POSITION = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("BLOCK_POSITION").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "p")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPTIONAL_BLOCK_POS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "p")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "o")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "n")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "m")
|
||||
public static final EntityDataSerializer<?> OPTIONAL_BLOCK_POSITION = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("OPTIONAL_BLOCK_POSITION").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "q")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "DIRECTION")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "q")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "p")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "o")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "n")
|
||||
@@ -163,11 +202,13 @@ public class WrappedEntityData extends Wrapper
|
||||
public static final EntityDataSerializer<Optional<UUID>> OPTIONAL_UUID = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("OPTIONAL_UUID").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9), path = "r")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPTIONAL_LIVING_ENTITY_REFERENCE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "r")
|
||||
public static final EntityDataSerializer<Optional<?>> OPTIONAL_ENTITY_REFERENCE = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("OPTIONAL_ENTITY_REFERENCE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_9), path = "s")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPTIONAL_GLOBAL_POS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "s")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "r")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "q")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "p")
|
||||
@@ -181,7 +222,8 @@ public class WrappedEntityData extends Wrapper
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "p")
|
||||
public static final EntityDataSerializer<?> NBT_TAG = new EntityDataSerializer<>(getStaticWrappedFieldValue("NBT_TAG").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "t")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "VILLAGER_DATA")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "t")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_6), path = "u")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "t")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "s")
|
||||
@@ -190,7 +232,8 @@ public class WrappedEntityData extends Wrapper
|
||||
public static final EntityDataSerializer<?> VILLAGER_DATA = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("VILLAGER_DATA").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "u")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "OPTIONAL_UNSIGNED_INT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "u")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_6), path = "v")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "u")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "t")
|
||||
@@ -199,7 +242,8 @@ public class WrappedEntityData extends Wrapper
|
||||
public static final EntityDataSerializer<OptionalInt> OPTIONAL_INT = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("OPTIONAL_INT").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "v")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "POSE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "v")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_6), path = "w")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "v")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "u")
|
||||
@@ -208,97 +252,35 @@ public class WrappedEntityData extends Wrapper
|
||||
public static final EntityDataSerializer<Pose> ENTITY_POSE = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("ENTITY_POSE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "w")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_6), path = "x")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "w")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "v")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "u")
|
||||
public static final EntityDataSerializer<?> CAT_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("CAT_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "x")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "y")
|
||||
public static final EntityDataSerializer<?> CHICKEN_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("CHICKEN_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "y")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "z")
|
||||
public static final EntityDataSerializer<?> COW_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("COW_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "z")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "A")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "y")
|
||||
public static final EntityDataSerializer<?> WOLF_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("WOLF_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "A")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "B")
|
||||
public static final EntityDataSerializer<?> WOLF_SOUND_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("WOLF_SOUND_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "B")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "C")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "z")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "x")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "w")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "v")
|
||||
public static final EntityDataSerializer<?> FROG_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("FROG_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "C")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "D")
|
||||
public static final EntityDataSerializer<?> PIG_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("PIG_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "D")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "E")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "A")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "y")
|
||||
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "x")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "w")
|
||||
public static final EntityDataSerializer<?> PAINTING_VARIANT_HOLDER = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("PAINTING_VARIANT_HOLDER").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "E")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "F")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "B")
|
||||
public static final EntityDataSerializer<?> ARMADILLO_STATE = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("ARMADILLO_STATE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "F")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "G")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "C")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "z")
|
||||
public static final EntityDataSerializer<?> SNIFFER_STATE = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("SNIFFER_STATE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "G")
|
||||
public static final EntityDataSerializer<?> COPPER_STATE = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("COPPER_STATE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "H")
|
||||
public static final EntityDataSerializer<?> COPPER_GOLEM_STATE = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("COPPER_GOLEM_STATE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "I")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "VECTOR3")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "J")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "I")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "H")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "D")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "A")
|
||||
public static final EntityDataSerializer<Vector3f> VECTOR3 = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("VECTOR3").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "J")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "QUARTERNION")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "K")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "J")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_6), path = "I")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "E")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "B")
|
||||
public static final EntityDataSerializer<?> QUARTERNION = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("QUARTERNION").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "K")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "RESOLVABLE_PROFILE")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "L")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "K")
|
||||
public static final EntityDataSerializer<?> RESOLVABLE_PROFILE = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("RESOLVABLE_PROFILE").orElse(null));
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "HUMANOID_ARM")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "M")
|
||||
public static final EntityDataSerializer<?> HAND = new EntityDataSerializer<>(
|
||||
getStaticWrappedFieldValue("HAND").orElse(null));
|
||||
|
||||
private EntityDataSerializers()
|
||||
{
|
||||
super(null);
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package de.eisi05.npc.api.wrapper.objects;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import de.eisi05.npc.api.utils.Reflections;
|
||||
import de.eisi05.npc.api.utils.SerializableFunction;
|
||||
import de.eisi05.npc.api.utils.Var;
|
||||
import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Represents a snapshot of an entity's state that can be serialized and later used to restore the entity. This is particularly useful for NPCs and other custom
|
||||
* entities that need to be saved and loaded.
|
||||
*/
|
||||
public class WrappedEntitySnapshot implements Serializable
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String type;
|
||||
private final byte[] data;
|
||||
private final SerializableFunction<? extends Entity, ? extends Entity> entityFunction;
|
||||
|
||||
/**
|
||||
* Creates a new entity snapshot with the specified entity type and NBT data.
|
||||
*
|
||||
* @param type The type of entity this snapshot represents
|
||||
* @param data The NBT data containing the entity's properties (can be null)
|
||||
*/
|
||||
public WrappedEntitySnapshot(@NotNull EntityType type, @Nullable WrappedCompoundTag data)
|
||||
{
|
||||
this.type = type.name();
|
||||
this.data = data == null ? null : data.getData();
|
||||
this.entityFunction = entity -> entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new entity snapshot with a custom entity transformation function.
|
||||
*
|
||||
* @param type The type of entity this snapshot represents
|
||||
* @param function A function that transforms the created entity before it's used
|
||||
*/
|
||||
public WrappedEntitySnapshot(@NotNull EntityType type, @NotNull SerializableFunction<? extends Entity, ? extends Entity> function)
|
||||
{
|
||||
this.type = type.name();
|
||||
this.data = null;
|
||||
this.entityFunction = function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new entity snapshot with default NBT data.
|
||||
*
|
||||
* @param type The type of entity this snapshot represents
|
||||
*/
|
||||
public WrappedEntitySnapshot(@NotNull EntityType type)
|
||||
{
|
||||
this(type, (WrappedCompoundTag) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of entity this snapshot represents.
|
||||
*
|
||||
* @return The entity type
|
||||
*/
|
||||
public @NotNull EntityType getType()
|
||||
{
|
||||
return EntityType.valueOf(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NBT data containing the entity's properties.
|
||||
*
|
||||
* @return The entity's NBT data
|
||||
*/
|
||||
public @NotNull WrappedCompoundTag getData()
|
||||
{
|
||||
return data == null ? new WrappedCompoundTag() : WrappedCompoundTag.parse(data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new entity in the specified world using this snapshot's data.
|
||||
*
|
||||
* @param world The world to create the entity in
|
||||
* @return A wrapped entity instance
|
||||
* @throws RuntimeException If the entity could not be created
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NotNull WrappedEntity<?> create(@NotNull World world)
|
||||
{
|
||||
WrappedEntity<?> entity;
|
||||
WrappedCompoundTag data = getData();
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_2))
|
||||
{
|
||||
data.putString("id", type.toLowerCase());
|
||||
|
||||
entity = new WrappedEntity<>(Reflections.invokeStaticMethod("net.minecraft.world.entity.EntityTypes", "a",
|
||||
data.getHandle(), Var.getNmsLevel(world), Function.identity()).get());
|
||||
}
|
||||
else if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
|
||||
{
|
||||
data.putString("id", type.toLowerCase());
|
||||
entity = new WrappedEntity<>(
|
||||
Reflections.invokeStaticMethod("net.minecraft.world.entity.EntityTypes", "a",
|
||||
data.getHandle(), Var.getNmsLevel(world), WrappedEntity.SpawnReason.LOAD.getHandle(), Function.identity()).get());
|
||||
}
|
||||
else if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_11))
|
||||
entity = new WrappedEntity<>(
|
||||
Reflections.invokeStaticMethod("net.minecraft.world.entity.EntityTypes", "a", Var.toNmsEntityType(getType()), data.getHandle(),
|
||||
Var.getNmsLevel(world), WrappedEntity.SpawnReason.LOAD.getHandle(), Function.identity()).get());
|
||||
else if(Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
entity = new WrappedEntity<>(
|
||||
Reflections.invokeStaticMethod("net.minecraft.world.entity.EntityTypes", "a", Var.toNmsEntityType(getType()),
|
||||
data.getHandle(), Var.getNmsLevel(world), WrappedEntity.SpawnReason.LOAD.getHandle(),
|
||||
WrappedEntity.EntityProcessor.NOP).get());
|
||||
else
|
||||
entity = new WrappedEntity<>(
|
||||
Reflections.invokeStaticMethod("net.minecraft.world.entity.EntityType", "loadEntityRecursive",
|
||||
Var.toNmsEntityType(getType()), data.getHandle(), Var.getNmsLevel(world), WrappedEntity.SpawnReason.LOAD.getHandle(),
|
||||
WrappedEntity.EntityProcessor.NOP).get());
|
||||
|
||||
WrappedEntity<?> finalEntity = entityFunction == null ? entity :
|
||||
WrappedEntity.fromEntity(entityFunction.apply(Var.unsafeCast(entity.getBukkitPlayer())), WrappedEntity.class);
|
||||
finalEntity.data = data.toString();
|
||||
return finalEntity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A wrapper for Minecraft's NBT (Named Binary Tag) compound tag system, providing a type-safe way to read and write NBT data.
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.nbt.CompoundTag")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.nbt.NBTTagCompound")
|
||||
public static class WrappedCompoundTag extends Wrapper
|
||||
{
|
||||
/**
|
||||
* Creates a new WrappedCompoundTag from a native NBT tag handle.
|
||||
*
|
||||
* @param handle The native NBT tag object
|
||||
*/
|
||||
private WrappedCompoundTag(Object handle)
|
||||
{
|
||||
super(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new, empty WrappedCompoundTag.
|
||||
*/
|
||||
private WrappedCompoundTag()
|
||||
{
|
||||
super(createInstance(WrappedCompoundTag.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON string into a WrappedCompoundTag.
|
||||
*
|
||||
* @param s The JSON string to parse
|
||||
* @return A new WrappedCompoundTag containing the parsed data
|
||||
* @throws CommandSyntaxException If the JSON is invalid
|
||||
*/
|
||||
public static WrappedCompoundTag parse(@NotNull String s) throws CommandSyntaxException
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
return new WrappedCompoundTag(Reflections.invokeStaticMethod("net.minecraft.nbt.TagParser", "parseCompoundFully", s).get());
|
||||
|
||||
String methodName = Versions.isCurrentVersionSmallerThan(Versions.V1_18) ? "parse" : "a";
|
||||
return new WrappedCompoundTag(Reflections.invokeStaticMethod("net.minecraft.nbt.MojangsonParser", methodName, s).get());
|
||||
}
|
||||
catch(RuntimeException e1)
|
||||
{
|
||||
if(e1.getCause() instanceof InvocationTargetException e2 && e2.getCause() instanceof CommandSyntaxException e3)
|
||||
throw e3;
|
||||
throw e1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a byte array containing NBT data into a WrappedCompoundTag.
|
||||
*
|
||||
* @param bytes The NBT data as a byte array
|
||||
* @return A new WrappedCompoundTag containing the parsed data
|
||||
*/
|
||||
public static WrappedCompoundTag parse(byte[] bytes)
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_20_4))
|
||||
return new WrappedCompoundTag(
|
||||
Reflections.invokeStaticMethod("net.minecraft.nbt.NBTCompressedStreamTools", "a", new ByteArrayInputStream(bytes)).get());
|
||||
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
return new WrappedCompoundTag(
|
||||
Reflections.invokeStaticMethod("net.minecraft.nbt.NBTCompressedStreamTools", "a", new ByteArrayInputStream(bytes),
|
||||
NbtAccounter.unlimitedHeap().getHandle()).get());
|
||||
|
||||
return new WrappedCompoundTag(
|
||||
Reflections.invokeStaticMethod("net.minecraft.nbt.NbtIo", "readCompressed", new ByteArrayInputStream(bytes),
|
||||
NbtAccounter.unlimitedHeap().getHandle()).get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a string value in this compound tag.
|
||||
*
|
||||
* @param key The key to store the value under
|
||||
* @param value The string value to store
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "putString")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setString")
|
||||
public void putString(@NotNull String key, @NotNull String value)
|
||||
{
|
||||
invokeWrappedMethod(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a boolean value from this compound tag.
|
||||
*
|
||||
* @param key The key of the boolean value
|
||||
* @return The boolean value, or false if the key doesn't exist
|
||||
*/
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getBooleanOr")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "q")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getBoolean")
|
||||
public boolean getBoolean(@NotNull String key)
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5))
|
||||
return invokeWrappedMethod(key);
|
||||
return invokeWrappedMethod(key, false);
|
||||
}
|
||||
|
||||
private byte[] getData()
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V26_1))
|
||||
Reflections.invokeStaticMethod("net.minecraft.nbt.NBTCompressedStreamTools", "a", getHandle(), baos);
|
||||
else
|
||||
Reflections.invokeStaticMethod("net.minecraft.nbt.NbtIo", "writeCompressed", getHandle(), baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getHandle().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.nbt.NbtAccounter")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.nbt.NBTReadLimiter")
|
||||
public static class NbtAccounter extends Wrapper
|
||||
{
|
||||
private NbtAccounter(Object handle)
|
||||
{
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "unlimitedHeap")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_9), path = "a")
|
||||
public static NbtAccounter unlimitedHeap()
|
||||
{
|
||||
return new NbtAccounter(invokeStaticWrappedMethod());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_9), path = "net.minecraft.server.level.EntityTrackerEntry")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.server.level.ServerEntity")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_11), path = "net.minecraft.server.level.EntityTrackerEntry")
|
||||
public class WrappedEntityTrackerEntry extends Wrapper
|
||||
{
|
||||
public WrappedEntityTrackerEntry(@NotNull WrappedEntity<?> entity)
|
||||
|
||||
@@ -8,7 +8,7 @@ import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.server.MinecraftServer")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "net.minecraft.server.MinecraftServer")
|
||||
public class WrappedMinecraftServer extends Wrapper
|
||||
{
|
||||
public static final WrappedMinecraftServer INSTANCE = new WrappedMinecraftServer(Reflections.invokeMethod(Bukkit.getServer(), "getServer").get());
|
||||
@@ -18,13 +18,20 @@ public class WrappedMinecraftServer extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "vanillaCommandDispatcher")
|
||||
public static int getCurrentTick()
|
||||
{
|
||||
return Reflections.getStaticField(INSTANCE.handle.getClass(), "currentTick");
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "vanillaCommandDispatcher")
|
||||
public @NotNull WrappedMinecraftServer commands()
|
||||
{
|
||||
return new WrappedMinecraftServer(getWrappedFieldValue());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "bg")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "registryAccess")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "bc")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "bg")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_6), path = "ba")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21), path = "bc")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "aZ")
|
||||
@@ -37,7 +44,8 @@ public class WrappedMinecraftServer extends Wrapper
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getDispatcher")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NotNull CommandDispatcher<Object> getCommandDispatcher()
|
||||
{
|
||||
|
||||
@@ -7,13 +7,15 @@ import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import de.eisi05.npc.api.wrapper.enums.ChatFormat;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.scores.ScoreboardTeam")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.scores.PlayerTeam")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.scores.ScoreboardTeam")
|
||||
public class WrappedPlayerTeam extends Wrapper
|
||||
{
|
||||
private static final Map<UUID, Map<String, WrappedPlayerTeam>> teams = new HashMap<>();
|
||||
@@ -23,6 +25,20 @@ public class WrappedPlayerTeam extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
public static @NotNull WrappedPlayerTeam getPlayersTeam(@NotNull Player player)
|
||||
{
|
||||
return new WrappedPlayerTeam(Reflections.invokeMethod(player.getScoreboard(), "getHandle")
|
||||
.thanInvoke(switch(Versions.getVersion())
|
||||
{
|
||||
case NONE -> null;
|
||||
case V1_17 -> "getPlayerTeam";
|
||||
case V1_18, V1_18_2, V1_19, V1_19_1, V1_19_3, V1_19_4, V1_20 -> "i";
|
||||
case V1_20_2 -> "g";
|
||||
case V1_20_4, V1_20_6, V1_21, V1_21_2, V1_21_4, V1_21_5, V1_21_6, V1_21_9, V1_21_11 -> "e";
|
||||
case V26_1 -> "getPlayersTeam";
|
||||
}, WrappedServerPlayer.fromPlayer(player).getName()).get());
|
||||
}
|
||||
|
||||
public static @NotNull WrappedPlayerTeam create(@NotNull Player player, @NotNull String name)
|
||||
{
|
||||
if(exists(player, name))
|
||||
@@ -55,19 +71,66 @@ public class WrappedPlayerTeam extends Wrapper
|
||||
entry.getValue().keySet().removeIf(s -> s.equals(name));
|
||||
}
|
||||
|
||||
public static void clear(UUID uuid, String name)
|
||||
{
|
||||
teams.get(uuid).remove(name);
|
||||
}
|
||||
|
||||
public static boolean exists(@NotNull Player player, @NotNull String name)
|
||||
{
|
||||
return teams.getOrDefault(player.getUniqueId(), new HashMap<>()).containsKey(name);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getNameTagVisibility")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "j")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getNameTagVisibility")
|
||||
public @Nullable Visibility getNameTagVisibility()
|
||||
{
|
||||
Object enumHandle = getWrappedFieldValue();
|
||||
for(Visibility visibility : Visibility.values())
|
||||
{
|
||||
if(visibility.getHandle().equals(enumHandle))
|
||||
return visibility;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setNameTagVisibility")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setNameTagVisibility")
|
||||
public void setNameTagVisibility(@NotNull Visibility visibility)
|
||||
{
|
||||
invokeWrappedMethod(visibility.getHandle());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setCollisionRule")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setCollisionRule")
|
||||
public void setCollisionRule(@NotNull CollisionRule collisionRule)
|
||||
{
|
||||
invokeWrappedMethod(collisionRule.getHandle());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setSeeFriendlyInvisibles")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "b")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setCanSeeFriendlyInvisibles")
|
||||
public void setCanSeeFriendlyInvisible(boolean canSeeFriendlyInvisible)
|
||||
{
|
||||
invokeWrappedMethod(canSeeFriendlyInvisible);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "canSeeFriendlyInvisibles")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "j")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "i")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "canSeeFriendlyInvisibles")
|
||||
public boolean canSeeFriendlyInvisible()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getPlayers")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "g")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getPlayerNameSet")
|
||||
public @NotNull Collection<String> getPlayers()
|
||||
@@ -75,7 +138,8 @@ public class WrappedPlayerTeam extends Wrapper
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getName")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "b")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getName")
|
||||
public @NotNull String getName()
|
||||
@@ -83,34 +147,41 @@ public class WrappedPlayerTeam extends Wrapper
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setColor")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setColor")
|
||||
public void setColor(@NotNull ChatFormat color)
|
||||
{
|
||||
invokeWrappedMethod(color);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setPlayerPrefix")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setPrefix")
|
||||
public void setPrefix(@NotNull WrappedComponent prefix)
|
||||
{
|
||||
invokeWrappedMethod(prefix);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.scores" +
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.scores.Team$Visibility")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.scores" +
|
||||
".ScoreboardTeamBase$EnumNameTagVisibility")
|
||||
public enum Visibility implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ALWAYS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
ALWAYS,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "NEVER")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
NEVER,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "HIDE_FOR_OTHER_TEAMS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
HIDE_FOR_OTHER_TEAMS,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "HIDE_FOR_OWN_TEAM")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
HIDE_FOR_OWN_TEAM;
|
||||
|
||||
@Override
|
||||
@@ -119,4 +190,31 @@ public class WrappedPlayerTeam extends Wrapper
|
||||
return cast(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.scores.Team$CollisionRule")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.scores.ScoreboardTeamBase$EnumTeamPush")
|
||||
public enum CollisionRule implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ALWAYS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
ALWAYS,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "NEVER")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
NEVER,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "PUSH_OTHER_TEAMS")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
PUSH_OTHER_TEAMS,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "PUSH_OWN_TEAM")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
PUSH_OWN_TEAM;
|
||||
|
||||
@Override
|
||||
public @NotNull Object getHandle()
|
||||
{
|
||||
return cast(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.server.level.EntityPlayer")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.server.level.ServerPlayer")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.server.level.EntityPlayer")
|
||||
public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
{
|
||||
private static final Map<UUID, WrappedServerPlayer> map = new HashMap<>();
|
||||
@@ -39,9 +40,9 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
}
|
||||
|
||||
public static @NotNull WrappedServerPlayer create(@NotNull Location location, @NotNull UUID uuid, @NotNull GameProfile gameProfile,
|
||||
@NotNull WrappedComponent name)
|
||||
@NotNull WrappedComponent name, @Nullable WrappedNameTag<?> nameDisplay)
|
||||
{
|
||||
if(map.containsKey(uuid))
|
||||
if(map.containsKey(uuid) && nameDisplay == null)
|
||||
return map.get(uuid);
|
||||
|
||||
Object mcServer = WrappedMinecraftServer.INSTANCE.getHandle();
|
||||
@@ -53,10 +54,9 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
|
||||
WrappedServerPlayer wrappedServerPlayer = switch(Versions.getVersion())
|
||||
{
|
||||
case V1_17, V1_18, V1_18_2, V1_19_3, V1_19_4, V1_20 ->
|
||||
createWrappedInstance(WrappedServerPlayer.class, mcServer, serverLevel, gameProfile);
|
||||
case V1_17, V1_18, V1_18_2, V1_19_3, V1_19_4, V1_20 -> createWrappedInstance(WrappedServerPlayer.class, mcServer, serverLevel, gameProfile);
|
||||
case V1_19, V1_19_1 -> createWrappedInstance(WrappedServerPlayer.class, mcServer, serverLevel, gameProfile, null);
|
||||
case V1_20_2, V1_20_4, V1_20_6, V1_21, V1_21_2, V1_21_4, V1_21_5, V1_21_6, V1_21_9 ->
|
||||
case V1_20_2, V1_20_4, V1_20_6, V1_21, V1_21_2, V1_21_4, V1_21_5, V1_21_6, V1_21_9, V1_21_11, V26_1 ->
|
||||
{
|
||||
WrappedServerPlayer serverPlayer = createWrappedInstance(WrappedServerPlayer.class, mcServer, serverLevel, gameProfile,
|
||||
WrappedClientInformation.createDefault().getHandle());
|
||||
@@ -70,7 +70,9 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
case NONE -> throw new VersionNotFound();
|
||||
};
|
||||
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_4))
|
||||
if(nameDisplay != null)
|
||||
wrappedServerPlayer.setNameTag(nameDisplay);
|
||||
else if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_4))
|
||||
{
|
||||
WrappedArmorStand armorStand = WrappedArmorStand.create(location.getWorld());
|
||||
armorStand.moveTo(location.clone().add(0, 0.2, 0));
|
||||
@@ -89,12 +91,14 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
return wrappedServerPlayer;
|
||||
}
|
||||
|
||||
public static @NotNull WrappedServerPlayer create(@NotNull Location location, @NotNull UUID uuid, @NotNull WrappedComponent name)
|
||||
public static @NotNull WrappedServerPlayer create(@NotNull Location location, @NotNull UUID uuid, @NotNull WrappedComponent name,
|
||||
@Nullable WrappedNameTag<?> nameDisplay)
|
||||
{
|
||||
return create(location, uuid, new GameProfile(uuid, "NPC" + uuid.toString().substring(0, 13)), name);
|
||||
return create(location, uuid, new GameProfile(uuid, "NPC" + uuid.toString().substring(0, 13)), name, nameDisplay);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getAttribute")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21), path = "f")
|
||||
public @NotNull WrappedAttributeInstance getAttribute(Object attribute)
|
||||
@@ -102,7 +106,9 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
return new WrappedAttributeInstance(invokeWrappedMethod(attribute));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "gz")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getGameProfile")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "gI")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "gz")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "gr")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_5), path = "gi")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "gh")
|
||||
@@ -123,7 +129,8 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "connection")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20, to = Versions.V1_21), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_4), path = "b")
|
||||
@@ -132,7 +139,8 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
return new WrappedConnection(getWrappedFieldValue());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "connection")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_6, to = Versions.V1_21_11), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20, to = Versions.V1_21), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_4), path = "b")
|
||||
@@ -141,19 +149,19 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
setWrappedFieldValue(newConnection.getHandle());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "listName")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "listName")
|
||||
public void setListName(@Nullable WrappedComponent component)
|
||||
{
|
||||
setWrappedFieldValue(component.getHandle());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "listName")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "listName")
|
||||
public void setListName(@Nullable String name)
|
||||
{
|
||||
setListName(WrappedComponent.create(name));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_9), path = "listOrder")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V26_1), path = "listOrder")
|
||||
public void setListOrder(int order)
|
||||
{
|
||||
setWrappedFieldValue(order);
|
||||
@@ -170,14 +178,16 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
new WrappedConnection.CommonListenerCookie(getGameProfile(), i, true)));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setPose")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "b")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setPose")
|
||||
public void setPose(@NotNull Pose pose)
|
||||
{
|
||||
invokeWrappedMethod(pose);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "C")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "createCommandSourceStack")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "C")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "A")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_5), path = "z")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "A")
|
||||
@@ -196,7 +206,30 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
return new WrappedCommandSourceStack(invokeWrappedMethod());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "stopUsingItem")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "gf")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "fU")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "fM")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "fF")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21), path = "fx")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_6), path = "fB")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "ft")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "fs")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20), path = "fo")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_4), path = "fk")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "ff")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_1), path = "eZ")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19), path = "fa")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_18_2), path = "eS")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_18), path = "eR")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "clearActiveItem")
|
||||
public void stopUsingItem()
|
||||
{
|
||||
invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "sendSystemMessage")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_11), path = "a")
|
||||
public void sendRawMessage(@NotNull WrappedComponent component)
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19))
|
||||
@@ -205,7 +238,8 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
invokeWrappedMethod(component);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "remove")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_11), path = "a")
|
||||
public void remove()
|
||||
{
|
||||
map.remove(getUUID());
|
||||
@@ -214,6 +248,26 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
Reflections.invokeMethod(nmsLevel, getPath(), getHandle(), RemovalReason.DISCARDED.getHandle());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getAdvancements-stopListening")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "U-a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "S-a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_5), path = "R-a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "S-a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21), path = "R-a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_20_6), path = "Q-a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "O-a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20), path = "M-a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "N-a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_19_1), path = "M-a")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getAdvancementData-a")
|
||||
public void stopAdvancementListening()
|
||||
{
|
||||
String path = getPath();
|
||||
String[] paths = path.split("-");
|
||||
|
||||
Reflections.invokeMethod(getHandle(), paths[0]).thanInvoke(paths[1]);
|
||||
}
|
||||
|
||||
public @NotNull String getName()
|
||||
{
|
||||
return (String) Reflections.getField(getGameProfile(), "name").get();
|
||||
@@ -250,17 +304,4 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
setPassengers(nameDisplay);
|
||||
this.nameDisplay = nameDisplay;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "net.minecraft.world.entity.Entity$RemovalReason")
|
||||
private enum RemovalReason implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_9), path = "b")
|
||||
DISCARDED;
|
||||
|
||||
@Override
|
||||
public @NotNull Object getHandle()
|
||||
{
|
||||
return cast(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_9), path = "net.minecraft.world.entity.Display$TextDisplay")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V26_1), path = "net.minecraft.world.entity.Display$TextDisplay")
|
||||
public class WrappedTextDisplay extends WrappedEntity.WrappedNameTag<Entity>
|
||||
{
|
||||
private static final boolean flag = Versions.isCurrentVersionSmallerThan(Versions.V1_20_2);
|
||||
|
||||
@@ -4,7 +4,8 @@ import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.phys.Vec2F")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.phys.Vec2")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.phys.Vec2F")
|
||||
|
||||
public class WrappedVec2F extends Wrapper
|
||||
{
|
||||
@@ -13,15 +14,19 @@ public class WrappedVec2F extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "x")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "j")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "i")
|
||||
public float getYaw()
|
||||
{
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "j")
|
||||
public float getPitch()
|
||||
{
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "y")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "j")
|
||||
public float getYaw()
|
||||
{
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import de.eisi05.npc.api.wrapper.Wrapper;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.world.phys.Vec3D")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.phys.Vec3")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.phys.Vec3D")
|
||||
public class WrappedVec3D extends Wrapper
|
||||
{
|
||||
private WrappedVec3D(Object handle)
|
||||
@@ -29,7 +30,8 @@ public class WrappedVec3D extends Wrapper
|
||||
return new WrappedVec3D(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "x")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_6), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21), path = "c")
|
||||
public double getX()
|
||||
@@ -37,7 +39,8 @@ public class WrappedVec3D extends Wrapper
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "y")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_6), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21), path = "d")
|
||||
public double getY()
|
||||
@@ -45,7 +48,8 @@ public class WrappedVec3D extends Wrapper
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_9), path = "i")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "z")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "i")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_6), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21), path = "e")
|
||||
public double getZ()
|
||||
|
||||
@@ -2,25 +2,32 @@ package de.eisi05.npc.api.wrapper.packets;
|
||||
|
||||
import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game.PacketPlayOutAnimation")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.game.ClientboundAnimatePacket")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.game.PacketPlayOutAnimation")
|
||||
public class AnimatePacket extends PacketWrapper
|
||||
{
|
||||
protected AnimatePacket(@NotNull WrappedServerPlayer player, int animationId)
|
||||
protected AnimatePacket(@NotNull WrappedEntity<?> entity, int animationId)
|
||||
{
|
||||
super(AnimatePacket.class, player, animationId);
|
||||
super(AnimatePacket.class, entity, animationId);
|
||||
}
|
||||
|
||||
public static PacketWrapper create(@NotNull WrappedServerPlayer player, @NotNull Animation animation)
|
||||
public static @Nullable PacketWrapper create(@NotNull WrappedEntity<?> entity, @NotNull Animation animation)
|
||||
{
|
||||
if(animation != Animation.HURT || Versions.isCurrentVersionSmallerThan(Versions.V1_19_4))
|
||||
return new AnimatePacket(player, animation.ordinal());
|
||||
return new AnimatePacket(entity, animation.ordinal());
|
||||
|
||||
return new HurtAnimationPacket(player);
|
||||
if(!(entity.getBukkitPlayer() instanceof LivingEntity))
|
||||
return null;
|
||||
|
||||
return new HurtAnimationPacket(entity);
|
||||
}
|
||||
|
||||
public enum Animation implements Serializable
|
||||
@@ -33,13 +40,13 @@ public class AnimatePacket extends PacketWrapper
|
||||
MAGIC_CRITICAL_HIT
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_9),
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V26_1),
|
||||
path = "net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket")
|
||||
public static class HurtAnimationPacket extends PacketWrapper
|
||||
{
|
||||
private HurtAnimationPacket(@NotNull WrappedServerPlayer player)
|
||||
private HurtAnimationPacket(@NotNull WrappedEntity<?> entity)
|
||||
{
|
||||
super(HurtAnimationPacket.class, player);
|
||||
super(HurtAnimationPacket.class, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.eisi05.npc.api.wrapper.packets;
|
||||
|
||||
import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.wrapper.Mapping;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.game.ServerboundAttackPacket")
|
||||
public class AttackPacket extends PacketWrapper.PacketHolder
|
||||
{
|
||||
protected AttackPacket(Object handle)
|
||||
{
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "entityId")
|
||||
public int getId()
|
||||
{
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package de.eisi05.npc.api.wrapper.packets;
|
||||
import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.wrapper.Mapping;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game.PacketPlayOutEntity")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.game.PacketPlayOutEntity")
|
||||
public class MoveEntityPacket
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game" +
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Rot")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.game" +
|
||||
".PacketPlayOutEntity$PacketPlayOutEntityLook")
|
||||
public static class Rot extends PacketWrapper
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ public abstract class PacketWrapper extends Wrapper
|
||||
super(createInstance(clazz, args));
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.Packet")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "net.minecraft.network.protocol.Packet")
|
||||
public static abstract class PacketHolder extends PacketWrapper
|
||||
{
|
||||
public PacketHolder(Object handle)
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game" +
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V26_1), path = "net.minecraft.network.protocol.game" +
|
||||
".ClientboundPlayerInfoRemovePacket")
|
||||
public class PlayerInfoRemovePacket extends PacketWrapper
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game" +
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V26_1), path = "net.minecraft.network.protocol.game" +
|
||||
".ClientboundPlayerInfoUpdatePacket")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo")
|
||||
public class PlayerInfoUpdatePacket extends PacketWrapper
|
||||
@@ -15,40 +15,51 @@ public class PlayerInfoUpdatePacket extends PacketWrapper
|
||||
super(PlayerInfoUpdatePacket.class, action, serverPlayer);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game" +
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.game" +
|
||||
".ClientboundPlayerInfoUpdatePacket$Action")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.game" +
|
||||
".ClientboundPlayerInfoUpdatePacket$a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "net.minecraft.network.protocol.game" +
|
||||
".PacketPlayOutPlayerInfo$EnumPlayerInfoAction")
|
||||
public enum Action implements EnumWrapper
|
||||
{
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "a")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ADD_PLAYER")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "a")
|
||||
ADD_PLAYER,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "b")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "INITIALIZE_CHAT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "b")
|
||||
INITIALIZE_CHAT,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "UPDATE_GAME_MODE")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "c")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "b")
|
||||
UPDATE_GAME_MODE,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "d")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "UPDATE_LISTED")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "d")
|
||||
UPDATE_LISTED,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "UPDATE_LATENCY")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "e")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "c")
|
||||
UPDATE_LATENCY,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_9), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "UPDATE_DISPLAY_NAME")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_11), path = "f")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "d")
|
||||
UPDATE_DISPLAY_NAME,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "REMOVE_PLAYER")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "e")
|
||||
REMOVE_PLAYER,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_9), path = "g")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "UPDATE_LIST_ORDER")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_11), path = "g")
|
||||
UPDATE_LIST_ORDER,
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_4, to = Versions.V1_21_9), path = "h")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "UPDATE_HAT")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_4, to = Versions.V1_21_11), path = "h")
|
||||
UPDATE_HAT;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,8 +3,8 @@ package de.eisi05.npc.api.wrapper.packets;
|
||||
import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.wrapper.Mapping;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game" +
|
||||
".PacketPlayOutEntityDestroy")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.game.PacketPlayOutEntityDestroy")
|
||||
public class RemoveEntityPacket extends PacketWrapper
|
||||
{
|
||||
public RemoveEntityPacket(int entityId)
|
||||
|
||||
@@ -2,15 +2,15 @@ package de.eisi05.npc.api.wrapper.packets;
|
||||
|
||||
import de.eisi05.npc.api.utils.Versions;
|
||||
import de.eisi05.npc.api.wrapper.Mapping;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "net.minecraft.network.protocol.game" +
|
||||
".PacketPlayOutEntityHeadRotation")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.network.protocol.game.ClientboundRotateHeadPacket")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.game.PacketPlayOutEntityHeadRotation")
|
||||
public class RotateHeadPacket extends PacketWrapper
|
||||
{
|
||||
public RotateHeadPacket(@NotNull WrappedServerPlayer serverPlayer, byte yHeadRot)
|
||||
public RotateHeadPacket(@NotNull WrappedEntity entity, byte yHeadRot)
|
||||
{
|
||||
super(RotateHeadPacket.class, serverPlayer, yHeadRot);
|
||||
super(RotateHeadPacket.class, entity, yHeadRot);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user