Initial commit

This commit is contained in:
Eisi05 committed 2025-06-04 18:46:45 +02:00
commit 14ef4c9e13
66 files changed
+6705

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
---
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.
+20
View File
@@ -0,0 +1,20 @@
---
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.
+113
View File
@@ -0,0 +1,113 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# Common working directory
run/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Eisi05
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+173
View File
@@ -0,0 +1,173 @@
# NpcAPI
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.
## Features
- 🎭 Create custom NPCs with ease
- 🎨 Customize NPC appearance (skins, glowing effects, etc.)
- 👆 Handle click events and interactions
- 🎬 Play animations and control NPC behavior
- 💾 Save and load NPCs persistently
- 👥 Show/hide NPCs for specific players
- 🔍 Comprehensive NPC management system
## Installation
### Maven
Add the following repository and dependency to your `pom.xml`:
```xml
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<dependency>
<groupId>com.github.Eisi05</groupId>
<artifactId>NpcApi</artifactId>
<version>1.0</version>
</dependency>
```
### Gradle
Add the following to your `build.gradle`:
```gradle
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.Eisi05:NpcApi:Tag'
}
```
## Quick Start
### Plugin Setup
Initialize the NpcAPI in your plugin's main class:
```java
@Override
public void onEnable()
{
// Initialize NpcAPI with default configuration
NpcApi.createInstance(this, new NpcConfig());
}
@Override
public void onDisable()
{
// Properly disable NpcAPI
NpcApi.disable();
}
```
## Usage Examples
### Creating a Basic NPC
```java
// Create a location where the NPC should spawn
Location location = new Location(world, x, y, z);
// Create a new NPC with a name
NPC npc = new NPC(location, WrappedComponent.create("Test"));
// Enable the NPC to make it visible to all players
npc.setEnabled(true);
```
### Customizing NPC Appearance
```java
// Make the NPC glow with a red color
npc.setOption(NpcOption.GLOWING, ChatColor.RED);
// Set a custom skin from a player
npc.setOption(NpcOption.SKIN, Skin.fromPlayer(player));
```
### Handling Click Events
```java
// Set up a click event handler
npc.setClickEvent(event -> {
Player player = event.getPlayer();
NPC clickedNpc = event.getNpc();
player.sendMessage("You clicked " + clickedNpc.getName().toLegacy());
});
```
### Managing NPC State
```java
npc.save();
npc.setName(WrappedComponent.create("New Name"));
npc.setLocation(newLocation);
npc.reload();
```
### Advanced NPC Control
```java
npc.playAnimation(/* animation parameters */);
npc.showNPCToPlayer(player);
npc.hideNpcFromPlayer(player);
npc.lookAtPlayer(player);
npc.delete();
```
## NPC Management
### Getting All NPCs
```java
// Get a list of all available NPCs
List<NPC> allNpcs = NpcManager.getList();
```
### Finding NPCs by UUID
```java
// Get a specific NPC by its UUID
UUID npcUuid = /* your NPC's UUID */;
NPC npc = NpcManager.fromUUID(npcUuid);
```
## API Reference
### NPC Class
| Method | Description |
|---------------------------------------|------------------------------------------|
| `setOption(NpcOption, Object)` | Set NPC options like glowing, skin, etc. |
| `setClickEvent(Consumer<ClickEvent>)` | Set the click event handler |
| `setEnabled(boolean)` | Enable/disable NPC visibility |
| `save()` | Save NPC to persistent storage |
| `reload()` | Reload NPC data |
| `setName(WrappedComponent)` | Update NPC display name |
| `setLocation(Location)` | Move NPC to new location |
| `playAnimation(...)` | Play NPC animation |
| `showNPCToPlayer(Player)` | Show NPC to specific player |
| `hideNpcFromPlayer(Player)` | Hide NPC from specific player |
| `lookAtPlayer(Player)` | Make NPC look at player |
| `delete()` | Remove NPC permanently |
## Requirements
- Java 17+
- Spigot 1.17+ (compatible with newer versions)
- Minecraft server with NPC support
[![](https://jitpack.io/v/Eisi05/NpcApi.svg)](https://jitpack.io/#Eisi05/NpcApi)
+3
View File
@@ -0,0 +1,3 @@
jdk:
- openjdk17
+101
View File
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.eisi05</groupId>
<artifactId>NpcApi</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>NpcApi</name>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<defaultGoal>clean package</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>codemc-releases</id>
<url>https://repo.codemc.io/repository/maven-releases/</url>
</repository>
<repository>
<id>codemc-snapshots</id>
<url>https://repo.codemc.io/repository/maven-snapshots/</url>
</repository>
<repository>
<id>mojang</id>
<url>https://libraries.minecraft.net/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.17-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>4.2.1.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mojang</groupId>
<artifactId>datafixerupper</artifactId>
<version>6.0.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mojang</groupId>
<artifactId>authlib</artifactId>
<version>3.13.56</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mojang</groupId>
<artifactId>brigadier</artifactId>
<version>1.0.18</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
+102
View File
@@ -0,0 +1,102 @@
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.manager.NpcManager;
import de.eisi05.npc.api.objects.NPC;
import de.eisi05.npc.api.objects.NpcConfig;
import de.eisi05.npc.api.objects.Tasks;
import de.eisi05.npc.api.utils.PacketReader;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
/**
* 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
{
/**
* 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;
/**
* The configuration object for the NPC API, containing various settings
* like the look-at timer.
*/
public static NpcConfig config;
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.
*
* @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}.
*/
private NpcApi(@NotNull JavaPlugin plugin, @NotNull NpcConfig config)
{
NpcApi.plugin = plugin;
NpcApi.config = config;
Bukkit.getPluginManager().registerEvents(new ChangeWorldListener(), plugin);
Bukkit.getPluginManager().registerEvents(new ConnectionListener(), plugin);
Bukkit.getPluginManager().registerEvents(new NpcInteractListener(), plugin);
NpcManager.loadNPCs();
PacketReader.injectAll();
Tasks.start();
}
/**
* 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}.
*/
public static @NotNull NpcApi createInstance(@NotNull JavaPlugin plugin)
{
return createInstance(plugin, new NpcConfig());
}
/**
* 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}.
* @return The singleton {@link NpcApi} instance. Must not be {@code null}.
*/
public static @NotNull NpcApi createInstance(@NotNull JavaPlugin plugin, @NotNull NpcConfig config)
{
if(npcApi == null || plugin == null)
npcApi = new NpcApi(plugin, config);
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()
{
NpcManager.getList().forEach(NPC::hideNpcFromAllPlayers);
NpcManager.clear();
PacketReader.uninjectAll();
npcApi = null;
plugin = null;
}
}
@@ -0,0 +1,39 @@
package de.eisi05.npc.api.enums;
import org.jetbrains.annotations.NotNull;
import java.io.Serializable;
/**
* Represents the type of click action that can be performed on an NPC.
* This includes left click, right click, or both.
*/
public enum ClickActionType implements Serializable
{
/**
* Represents a left-click action.
*/
LEFT("Left"),
/**
* Represents a right-click action.
*/
RIGHT("Right"),
/**
* Represents both left and right-click actions.
*/
BOTH("Left & Right");
public final @NotNull String title;
/**
* Constructs a ClickActionType with the given title.
*
* @param title The display name for this click action type.
*/
ClickActionType(@NotNull String title)
{
this.title = title;
}
}
@@ -0,0 +1,50 @@
package de.eisi05.npc.api.enums;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import java.io.Serializable;
/**
* Represents the different customizable skin parts of an NPC,
* each associated with a unique byte value and an icon material.
*/
public enum SkinParts implements Serializable
{
CAPE((byte) 0x01, Material.ELYTRA),
JACKET((byte) 0x02, Material.LEATHER_CHESTPLATE),
LEFT_SLEEVE((byte) 0x04, Material.SHIELD),
RIGHT_SLEEVE((byte) 0x08, Material.DIAMOND_SWORD),
LEFT_PANTS_LEG((byte) 0x10, Material.LEATHER_LEGGINGS),
RIGHT_PANTS_LEG((byte) 0x20, Material.LEATHER_LEGGINGS),
HAT((byte) 0x40, Material.TURTLE_HELMET);
private final byte value;
private final Material icon;
SkinParts(byte value, @NotNull Material icon)
{
this.value = value;
this.icon = icon;
}
/**
* Returns the byte value representing this skin part.
*
* @return the bitmask value
*/
public byte getValue()
{
return value;
}
/**
* Returns the icon material associated with this skin part.
*
* @return the icon Material, never null
*/
public @NotNull Material getIcon()
{
return icon;
}
}
@@ -0,0 +1,96 @@
package de.eisi05.npc.api.events;
import de.eisi05.npc.api.enums.ClickActionType;
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;
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.
*/
public class NpcInteractEvent extends Event implements Serializable, Cancellable
{
private static final HandlerList HANDLERS = new HandlerList();
private final Player player;
private final NPC npc;
private final ClickActionType action;
private boolean cancelled;
/**
* Creates a new NpcInteractEvent.
*
* @param player the player who interacted with the NPC
* @param npc the NPC that was interacted with
* @param action the type of click action performed
*/
public NpcInteractEvent(@NotNull Player player, @NotNull NPC npc, @NotNull ClickActionType action)
{
this.player = player;
this.npc = npc;
this.action = action;
}
/**
* Returns the HandlerList for this event.
*
* @return the static HandlerList instance
*/
public static HandlerList getHandlerList()
{
return HANDLERS;
}
/**
* Returns the player who triggered this event.
*
* @return the interacting player, never null
*/
public @NotNull Player getPlayer()
{
return player;
}
/**
* Returns the NPC involved in this event.
*
* @return the interacted NPC, never null
*/
public @NotNull NPC getNpc()
{
return npc;
}
/**
* Returns the click action type of this interaction.
*
* @return the ClickActionType, never null
*/
public @NotNull ClickActionType getAction()
{
return action;
}
@Override
public @NotNull HandlerList getHandlers()
{
return getHandlerList();
}
@Override
public boolean isCancelled()
{
return cancelled;
}
@Override
public void setCancelled(boolean cancelled)
{
this.cancelled = cancelled;
}
}
@@ -0,0 +1,44 @@
package de.eisi05.npc.api.interfaces;
import de.eisi05.npc.api.events.NpcInteractEvent;
import org.jetbrains.annotations.NotNull;
import java.io.Serial;
import java.io.Serializable;
/**
* Functional interface representing an action to be performed
* when an NPC is clicked.
*/
@FunctionalInterface
public interface NpcClickAction extends Serializable
{
@Serial
long serialVersionUID = 1L;
/**
* Called when an NPC click event occurs.
*
* @param event the NpcInteractEvent containing interaction details
*/
void call(@NotNull NpcInteractEvent event);
/**
* 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.
*
* @return The initialized {@link NpcClickAction} instance. By default, it returns {@code this}.
*/
default NpcClickAction initialize() {return this;}
}
@@ -0,0 +1,17 @@
package de.eisi05.npc.api.listeners;
import de.eisi05.npc.api.manager.NpcManager;
import de.eisi05.npc.api.objects.NPC;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
public class ChangeWorldListener implements Listener
{
@EventHandler
public void onChange(PlayerChangedWorldEvent event)
{
for(NPC npc : NpcManager.getList())
npc.showNPCToPlayer(event.getPlayer());
}
}
@@ -0,0 +1,38 @@
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.utils.PacketReader;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;
public class ConnectionListener implements Listener
{
@EventHandler
public void onJoin(PlayerJoinEvent event)
{
PacketReader.inject(event.getPlayer());
new BukkitRunnable()
{
@Override
public void run()
{
NpcManager.getList().forEach(npc -> npc.showNPCToPlayer(event.getPlayer()));
}
}.runTaskLater(NpcApi.plugin, 10L);
}
@EventHandler
public void onLeave(PlayerQuitEvent event)
{
PacketReader.uninject(event.getPlayer());
for(NPC npc : NpcManager.getList())
npc.hideNpcFromPlayer(event.getPlayer());
}
}
@@ -0,0 +1,16 @@
package de.eisi05.npc.api.listeners;
import de.eisi05.npc.api.events.NpcInteractEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
public class NpcInteractListener implements Listener
{
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onClick(NpcInteractEvent event)
{
if(event.getNpc().getClickEvent() != null)
event.getNpc().getClickEvent().call(event);
}
}
@@ -0,0 +1,111 @@
package de.eisi05.npc.api.manager;
import de.eisi05.npc.api.NpcApi;
import de.eisi05.npc.api.objects.NPC;
import de.eisi05.npc.api.utils.ObjectSaver;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Manages the collection and lifecycle of NPC instances.
*/
public class NpcManager
{
private static final List<NPC> listNPC = new ArrayList<>();
/**
* Adds an NPC to the manager's list.
*
* @param npc the NPC to add
*/
public static void addNPC(@NotNull NPC npc)
{
listNPC.add(npc);
}
/**
* Returns the list of all managed NPCs.
*
* @return the list of NPCs
*/
public static @NotNull List<NPC> getList()
{
return listNPC;
}
/**
* Removes an NPC from the manager's list.
*
* @param npc the NPC to remove
*/
public static void removeNPC(@NotNull NPC npc)
{
listNPC.remove(npc);
}
/**
* Clears all NPCs from the manager.
*/
public static void clear()
{
listNPC.clear();
}
/**
* Finds an NPC by its UUID.
*
* @param uuid the UUID to search for
* @return an Optional containing the NPC if found, empty otherwise
*/
public static @NotNull Optional<NPC> fromUUID(@NotNull UUID uuid)
{
return listNPC.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.
*/
public static void loadNPCs()
{
File file = new File(NpcApi.plugin.getDataFolder(), "NPC");
File[] files = file.listFiles();
if(files == null)
return;
long failCounter = 0;
long successCounter = 0;
for(File file1 : files)
{
if(!file1.getName().endsWith(".npc"))
continue;
try
{
NPC.SerializedNPC serializedNPC = new ObjectSaver(file1).read();
serializedNPC.deserializedNPC().showNpcToAllPlayers();
successCounter++;
} catch(Exception e)
{
failCounter++;
}
}
if(successCounter == 1)
NpcApi.plugin.getLogger().info("Successfully loaded " + successCounter + " NPC");
else if(successCounter > 1)
NpcApi.plugin.getLogger().info("Successfully loaded " + successCounter + " NPC's");
if(failCounter == 1)
NpcApi.plugin.getLogger().warning("Failed to load " + failCounter + " NPC");
else if(failCounter > 1)
NpcApi.plugin.getLogger().warning("Failed to load " + failCounter + " NPC's");
}
}
@@ -0,0 +1,527 @@
package de.eisi05.npc.api.objects;
import de.eisi05.npc.api.NpcApi;
import de.eisi05.npc.api.interfaces.NpcClickAction;
import de.eisi05.npc.api.manager.NpcManager;
import de.eisi05.npc.api.utils.ObjectSaver;
import de.eisi05.npc.api.utils.Var;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.objects.WrappedArmorStand;
import de.eisi05.npc.api.wrapper.objects.WrappedComponent;
import de.eisi05.npc.api.wrapper.objects.WrappedPlayerTeam;
import de.eisi05.npc.api.wrapper.objects.WrappedServerPlayer;
import de.eisi05.npc.api.wrapper.packets.*;
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.File;
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
/**
* Represents a Non-Player Character (NPC) with location, appearance, options, and interaction logic.
*/
public class NPC extends NpcHolder
{
final List<Integer> toDeleteEntities = new ArrayList<>();
private final WrappedServerPlayer serverPlayer;
private final List<UUID> viewers = new ArrayList<>();
private final Map<NpcOption<?, ?>, Object> options;
private WrappedComponent name;
private Location location;
private NpcClickAction clickEvent;
private Instant createdAt = Instant.now();
/**
* Creates an NPC at the specified location with a random UUID and default name.
* The default name is an empty component.
*
* @param location the location to spawn the NPC. Must not be null.
*/
public NPC(@NotNull Location location)
{
this(location, UUID.randomUUID());
}
/**
* Creates an NPC at the specified location with a random UUID and given name.
*
* @param location the location to spawn the NPC. Must not be null.
* @param name the display name of the NPC. Must not be null.
*/
public NPC(@NotNull Location location, @NotNull WrappedComponent name)
{
this(location, UUID.randomUUID(), name);
}
/**
* Creates an NPC at the specified location with the given UUID and default name.
* The default name is an empty component.
*
* @param location the location to spawn the NPC. Must not be null.
* @param uuid the UUID of the NPC. Must not be null.
*/
public NPC(@NotNull Location location, @NotNull UUID uuid)
{
this(location, uuid, WrappedComponent.create(null));
}
/**
* Creates an NPC at the specified location with the given UUID and name.
* This is the primary constructor that initializes the NPC's core properties.
*
* @param location the location to spawn the NPC. Must not be null.
* @param uuid the UUID of the NPC. Must not be null.
* @param name the display name of the NPC. Must not be null.
*/
public NPC(@NotNull Location location, @NotNull UUID uuid, @NotNull WrappedComponent name)
{
this.name = name;
this.location = location;
this.serverPlayer = WrappedServerPlayer.create(location, uuid, name);
serverPlayer.moveTo(location);
this.options = new HashMap<>();
for(NpcOption<?, ?> value : NpcOption.values())
setOption(value, Var.unsafeCast(value.getDefaultValue()));
NpcManager.addNPC(this);
}
/**
* Private constructor used for creating a copy of an NPC.
*
* @param location The new location for the NPC. Must not be null.
* @param name The name for the NPC. Must not be null.
* @param options The options map for the NPC. Must not be null.
* @param clickEvent The click event for the NPC. Can be null.
*/
private NPC(@NotNull Location location, @NotNull WrappedComponent name, @NotNull Map<NpcOption<?, ?>, Object> options,
@Nullable NpcClickAction clickEvent)
{
this(location, UUID.randomUUID(), name);
this.options.putAll(options);
this.clickEvent = clickEvent;
}
/**
* Creates a copy of this NPC at a new location.
* The copied NPC will have a new UUID but will retain the original NPC's name, options, and click event.
*
* @param newLocation the location for the copied NPC. Must not be null.
* @return the new NPC instance. Will not be null.
*/
public @NotNull NPC copy(@NotNull Location newLocation)
{
return new NPC(newLocation, name.copy(), new HashMap<>(options), clickEvent == null ? null : clickEvent.copy());
}
/**
* Checks if this NPC has been saved to a file.
*
* @return {@code true} if the NPC's data file exists, {@code false} otherwise.
*/
public boolean isSaved()
{
return new File(NpcApi.plugin.getDataFolder(), "NPC\\" + getUUID() + ".npc").exists();
}
/**
* Saves the NPC's data to a file.
* This method serializes the NPC's current state and writes it to a .npc file.
*
* @throws IOException if an I/O error occurs during saving.
*/
@Override
public void save() throws IOException
{
new File(NpcApi.plugin.getDataFolder(), "NPC").mkdirs();
new ObjectSaver(new File(NpcApi.plugin.getDataFolder(), "NPC\\" + getUUID() + ".npc")).write(SerializedNPC.serializedNPC(this),
false);
super.save();
}
/**
* Gets the underlying server player representation for this NPC.
*
* @return the {@link WrappedServerPlayer} instance for this NPC. Will not be null.
*/
public @NotNull WrappedServerPlayer getServerPlayer()
{
return serverPlayer;
}
/**
* Gets the click action associated with this NPC.
*
* @return the {@link NpcClickAction} for this NPC, or {@code null} if no action is set.
*/
public @Nullable NpcClickAction getClickEvent()
{
return clickEvent;
}
/**
* Sets the click action for this NPC.
*
* @param event the {@link NpcClickAction} to set, or {@code null} to remove the current action.
* @return this NPC instance for method chaining. Will not be null.
*/
public @NotNull NPC setClickEvent(@Nullable NpcClickAction event)
{
this.clickEvent = event;
return this;
}
/**
* Checks if the NPC is currently enabled.
* An enabled NPC is visible and interactable (unless overridden by player permissions).
*
* @return {@code true} if the NPC is enabled, {@code false} otherwise.
*/
public boolean isEnabled()
{
return getOption(NpcOption.ENABLED);
}
/**
* Sets the enabled state of the NPC.
* Changing this state will trigger a reload of the NPC for all viewers.
*
* @param enabled {@code true} to enable the NPC, {@code false} to disable it.
*/
public void setEnabled(boolean enabled)
{
setOption(NpcOption.ENABLED, enabled);
reload();
}
/**
* Sets a specific option for this NPC.
*
* @param option the {@link NpcOption} to set. Must not be null.
* @param value the value for the option. If {@code null}, the option will be removed (reverting to default).
* @param <T> the type of the option's value.
*/
public <T> void setOption(@NotNull NpcOption<T, ?> option, @Nullable T value)
{
if(value == null)
options.remove(option);
else
options.put(option, value);
}
/**
* Gets the value of a specific option for this NPC.
* If the option has not been explicitly set, its default value will be returned.
*
* @param option the {@link NpcOption} to get. Must not be null.
* @param <T> the type of the option's value.
* @return the value of the option. Will not be null (guaranteed by NpcOption default values).
*/
@SuppressWarnings("unchecked")
public <T> @NotNull T getOption(@NotNull NpcOption<T, ?> option)
{
return (T) options.getOrDefault(option, option.getDefaultValue());
}
/**
* Plays an animation for this NPC, visible to the specified player.
*
* @param player the player who will see the animation. Must not be null.
* @param animation the {@link AnimatePacket.Animation} to play. Must not be null.
*/
public void playAnimation(@NotNull Player player, @NotNull AnimatePacket.Animation animation)
{
WrappedServerPlayer.fromPlayer(player).sendPacket(AnimatePacket.create(getServerPlayer(), animation));
}
/**
* Reloads the NPC for all current viewers.
* This typically involves hiding and then re-showing the NPC to apply any changes.
*/
public void reload()
{
final List<UUID> viewers = new ArrayList<>(this.viewers);
hideNpcFromAllPlayers();
viewers.stream().filter(uuid -> Bukkit.getPlayer(uuid) != null).forEach(uuid -> showNPCToPlayer(Bukkit.getPlayer(uuid)));
}
/**
* Gets the current location of the NPC.
*
* @return the {@link Location} of the NPC. Will not be null.
*/
public @NotNull Location getLocation()
{
return location;
}
/**
* Sets the location of the NPC.
* This will also update the underlying server player's position.
*
* @param location the new {@link Location} for the NPC. Must not be null.
*/
public void setLocation(@NotNull Location location)
{
this.location = location;
this.serverPlayer.moveTo(location);
}
/**
* Gets the unique identifier (UUID) of this NPC.
*
* @return the {@link UUID} of the NPC. Will not be null.
*/
public @NotNull UUID getUUID()
{
return serverPlayer.getUUID();
}
/**
* Gets the display name of this NPC.
*
* @return the {@link WrappedComponent} representing the NPC's name. Will not be null.
*/
public @NotNull WrappedComponent getName()
{
return name;
}
/**
* Sets the display name of this NPC.
* This also updates the name for the underlying server player and its list name.
*
* @param name the new {@link WrappedComponent} name for the NPC. Must not be null.
*/
public void setName(@NotNull WrappedComponent name)
{
this.name = name;
serverPlayer.setName(name.toLegacyNoColor());
serverPlayer.setListName(name);
}
/**
* Gets the timestamp when this NPC was created.
*
* @return the {@link Instant} of creation. Will not be null.
*/
public Instant getCreatedAt()
{
return createdAt;
}
/**
* Makes the NPC visible to all currently online players.
* This respects the NPC's enabled state and player permissions.
*/
public void showNpcToAllPlayers()
{
Bukkit.getOnlinePlayers().forEach(this::showNPCToPlayer);
}
/**
* Makes the NPC visible to a specific player.
* If the NPC is disabled and the player is not an operator, the NPC will not be shown.
* This method handles sending all necessary packets to display the NPC correctly.
*
* @param player the player to show the NPC to. Must not be null.
*/
public void showNPCToPlayer(@NotNull Player player)
{
if(!getOption(NpcOption.ENABLED) && !player.isOp())
return;
if(!player.getWorld().getName().equals(serverPlayer.getWorld().getName()))
{
hideNpcFromPlayer(player);
return;
}
if(!viewers.contains(player.getUniqueId()))
viewers.add(player.getUniqueId());
List<PacketWrapper> packets = new ArrayList<>();
packets.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.ADD_PLAYER, serverPlayer));
packets.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, serverPlayer));
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_19_3))
packets.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LISTED, serverPlayer));
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_2))
packets.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER, serverPlayer));
packets.add(serverPlayer.getAddEntityPacket());
WrappedPlayerTeam wrappedPlayerTeam = WrappedPlayerTeam.create(player, getServerPlayer().getName());
wrappedPlayerTeam.setNameTagVisibility(WrappedPlayerTeam.Visibility.NEVER);
packets.add(SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, true));
packets.add(SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, getServerPlayer().getName(), SetPlayerTeamPacket.Action.ADD));
packets.add(new RotateHeadPacket(serverPlayer, (byte) ((location.getYaw() % 360) * 256 / 360)));
packets.add(new MoveEntityPacket.Rot(serverPlayer.getId(), (byte) location.getYaw(), (byte) location.getPitch(), serverPlayer.isOnGround()));
Arrays.stream(NpcOption.values()).filter(npcOption -> !npcOption.equals(NpcOption.ENABLED))
.forEach(npcOption -> npcOption.getPacket(getOption(npcOption), this, player).ifPresent(packets::add));
NpcOption.ENABLED.getPacket(isEnabled(), this, player).ifPresent(packets::add);
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21))
serverPlayer.getNameTag()
.moveTo(getLocation().clone().add(0, (serverPlayer.getBoundingBox().getYSize() * getOption(NpcOption.SCALE)), 0));
packets.add(serverPlayer.getNameTag().getAddEntityPacket());
packets.add(
SetEntityDataPacket.create(serverPlayer.getNameTag().getId(), WrappedArmorStand.applyData(serverPlayer.getNameTag(), name), true));
if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21))
packets.add(new SetPassengerPacket(serverPlayer));
WrappedServerPlayer wrappedServerPlayer = WrappedServerPlayer.fromPlayer(player);
packets.forEach(wrappedServerPlayer::sendPacket);
}
/**
* Hides the NPC from all currently online players.
*/
public void hideNpcFromAllPlayers()
{
Bukkit.getOnlinePlayers().forEach(this::hideNpcFromPlayer);
}
/**
* Hides the NPC from a specific player.
* This method sends packets to remove the NPC and its associated entities from the player's view.
*
* @param player the player to hide the NPC from. Must not be null.
*/
public void hideNpcFromPlayer(@NotNull Player player)
{
WrappedServerPlayer wrappedServerPlayer = WrappedServerPlayer.fromPlayer(player);
wrappedServerPlayer.sendPacket(new RemoveEntityPacket(serverPlayer.getId()));
wrappedServerPlayer.sendPacket(new RemoveEntityPacket(serverPlayer.getNameTag().getId()));
toDeleteEntities.forEach(integer -> wrappedServerPlayer.sendPacket(new RemoveEntityPacket(integer)));
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_3))
wrappedServerPlayer.sendPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.REMOVE_PLAYER, serverPlayer));
else
wrappedServerPlayer.sendPacket(new PlayerInfoRemovePacket(List.of(getUUID())));
viewers.remove(player.getUniqueId());
}
/**
* Deletes the NPC.
* This hides the NPC from all players, removes it from the NPC manager, and deletes its saved data file.
*/
public void delete()
{
hideNpcFromAllPlayers();
NpcManager.removeNPC(this);
new File(NpcApi.plugin.getDataFolder(), "NPC").mkdirs();
File file = new File(NpcApi.plugin.getDataFolder(), "NPC\\" + getUUID() + ".npc");
if(file.exists())
file.delete();
}
/**
* Makes the NPC look at a specific player.
* This calculates the required yaw and pitch and sends update packets to the viewing player.
*
* @param viewer the player the NPC should look at. Must not be null.
*/
public void lookAtPlayer(@NotNull Player viewer)
{
Location npcLoc = serverPlayer.getBukkitPlayer().getLocation();
Location playerLoc = viewer.getLocation();
if(npcLoc.getWorld() != playerLoc.getWorld())
return;
double dx = playerLoc.getX() - npcLoc.getX();
double dy = ((playerLoc.getY() + viewer.getEyeHeight())) -
((npcLoc.getY() + serverPlayer.getBukkitPlayer().getEyeHeight() * getOption(NpcOption.SCALE)));
double dz = playerLoc.getZ() - npcLoc.getZ();
double distanceXZ = Math.sqrt(dx * dx + dz * dz);
float yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
float pitch = (float) Math.toDegrees(-Math.atan2(dy, distanceXZ));
byte yawByte = (byte) (yaw * 256 / 360);
byte pitchByte = (byte) (pitch * 256 / 360);
WrappedServerPlayer player = WrappedServerPlayer.fromPlayer(viewer);
player.sendPacket(new RotateHeadPacket(serverPlayer, yawByte));
player.sendPacket(new MoveEntityPacket.Rot(serverPlayer.getId(), yawByte, pitchByte, serverPlayer.isOnGround()));
}
/**
* A serializable representation of an NPC, used for saving and loading NPC data.
* This record stores all essential properties of an NPC that need to be persisted.
*
* @param world The UUID of the world where the NPC is located.
* @param x The x-coordinate of the NPC's location.
* @param y The y-coordinate of the NPC's location.
* @param z The z-coordinate of the NPC's location.
* @param yaw The yaw (horizontal rotation) of the NPC.
* @param pitch The pitch (vertical rotation) of the NPC.
* @param id The unique identifier (UUID) of the NPC.
* @param name The serialized representation of the NPC's display name.
* @param options A map of NPC options, where keys are option paths (strings) and values are serializable option values.
* @param clickEvent The click action associated with the NPC. Can be null.
* @param createdAt The timestamp when the NPC was originally created.
*/
public record SerializedNPC(@NotNull UUID world, double x, double y, double z, float yaw, float pitch, @NotNull UUID id,
@NotNull WrappedComponent.SerializedComponent name, @NotNull Map<String, ? extends Serializable> options,
@Nullable NpcClickAction clickEvent, @NotNull Instant createdAt) implements Serializable
{
@Serial
private static final long serialVersionUID = 1L;
/**
* Creates a {@link SerializedNPC} instance from an existing {@link NPC} object.
*
* @param npc The NPC to serialize. Must not be null.
* @return A new {@link SerializedNPC} instance representing the given NPC. Will not be null.
*/
public static @NotNull SerializedNPC serializedNPC(@NotNull NPC npc)
{
return new SerializedNPC(npc.getLocation().getWorld().getUID(), npc.getLocation().getX(), npc.getLocation().getY(),
npc.getLocation().getZ(), npc.getLocation().getYaw(), npc.getLocation().getPitch(), npc.getUUID(),
npc.getName().serialize(), npc.options.keySet().stream().collect(
Collectors.toMap(NpcOption::getPath, npcOption -> npcOption.serialize(npc.getOption((NpcOption<?, ?>) npcOption)))),
npc.clickEvent, npc.createdAt);
}
/**
* Deserializes this {@link SerializedNPC} object back into a fully functional {@link NPC} instance.
*
* @param <T> The type of the NpcOption value.
* @param <S> The serializable type of the NpcOption value.
* @return A new {@link NPC} instance reconstructed from the serialized data. Will not be null.
*/
@SuppressWarnings("unchecked")
public <T, S extends Serializable> @NotNull NPC deserializedNPC()
{
NPC npc = new NPC(new Location(Bukkit.getWorld(world), x, y, z, yaw, pitch), id, name.deserialize()).setClickEvent(
clickEvent == null ? clickEvent : clickEvent.initialize());
options.forEach((string, serializable) -> NpcOption.getOption(string)
.ifPresent(npcOption -> npc.setOption((NpcOption<T, S>) npcOption, (T) npcOption.deserialize(Var.unsafeCast(serializable)))));
npc.createdAt = createdAt == null ? Instant.now() : createdAt;
return npc;
}
}
}
@@ -0,0 +1,38 @@
package de.eisi05.npc.api.objects;
import org.jetbrains.annotations.NotNull;
/**
* 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.
*/
private long lookAtTimer = 5;
/**
* Sets the duration an NPC will look at a player after an interaction.
*
* @param time The time in ticks. For example, 20 ticks = 1 second.
* @return This {@link NpcConfig} instance for method chaining. Will not be null.
*/
public @NotNull NpcConfig lookAtTimer(long time)
{
lookAtTimer = time;
return this;
}
/**
* Gets the configured duration an NPC will look at a player.
*
* @return The time in ticks.
*/
public long getLookAtTimer()
{
return lookAtTimer;
}
}
@@ -0,0 +1,62 @@
package de.eisi05.npc.api.objects;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
/**
* 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
{
/**
* Flag indicating whether there are unsaved changes to this holder.
* Defaults to {@code false}.
*/
private boolean unsavedChanges = false;
/**
* Checks if there are any unsaved changes for this NPC holder.
*
* @return {@code true} if there are unsaved changes, {@code false} otherwise.
*/
public boolean hasUnsavedChanges()
{
return unsavedChanges;
}
/**
* 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()
{
unsavedChanges = true;
}
/**
* 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.
*/
public void save() throws IOException
{
unsavedChanges = false;
}
/**
* @throws UnsupportedOperationException always, as this inventory is not used.
*/
@Override
public @NotNull Inventory getInventory()
{
throw new UnsupportedOperationException("This inventory is not used!");
}
}
@@ -0,0 +1,448 @@
package de.eisi05.npc.api.objects;
import com.mojang.authlib.properties.Property;
import com.mojang.datafixers.util.Pair;
import de.eisi05.npc.api.NpcApi;
import de.eisi05.npc.api.enums.SkinParts;
import de.eisi05.npc.api.utils.ItemSerializer;
import de.eisi05.npc.api.utils.TriFunction;
import de.eisi05.npc.api.utils.Var;
import de.eisi05.npc.api.utils.Versions;
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.entity.Player;
import org.bukkit.entity.Pose;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 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.
*/
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.
*/
public static final NpcOption<Boolean, Boolean> USE_PLAYER_SKIN = new NpcOption<>("use-player-skin", false,
aBoolean -> aBoolean, aBoolean -> aBoolean,
(skin, npc, player) ->
{
if(!skin)
return null;
var textureProperties = WrappedServerPlayer.fromPlayer(player).getGameProfile().getProperties().get("textures").iterator();
npc.getServerPlayer().getGameProfile().getProperties().removeAll("textures");
if(!textureProperties.hasNext())
return null;
var textureProperty = textureProperties.next();
npc.getServerPlayer().getGameProfile().getProperties().put("textures", textureProperty);
return null;
});
/**
* 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<Skin, Skin> SKIN = new NpcOption<>("skin", null,
skin -> skin, skin -> skin,
(skin, npc, player) ->
{
if(npc.getOption(USE_PLAYER_SKIN))
return null;
npc.getServerPlayer().getGameProfile().getProperties().removeAll("textures");
if(skin == null)
return null;
npc.getServerPlayer().getGameProfile().getProperties()
.put("textures", new Property("textures", skin.value(), skin.signature()));
return null;
});
/**
* 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,
(show, npc, player) ->
{
if(show)
return null;
new BukkitRunnable()
{
@Override
public void run()
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_3))
WrappedServerPlayer.fromPlayer(player).sendPacket(
new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.REMOVE_PLAYER, npc.getServerPlayer()));
else
WrappedServerPlayer.fromPlayer(player)
.sendPacket(new PlayerInfoRemovePacket(List.of(npc.getUUID())));
}
}.runTaskLater(NpcApi.plugin, 50);
return null;
});
/**
* 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,
(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,
(hide, npc, player) ->
{
WrappedEntityData data = npc.getServerPlayer().getNameTag().getEntityData();
data.set(WrappedEntityData.EntityDataSerializers.BOOLEAN.create(3), !hide);
return SetEntityDataPacket.create(npc.getServerPlayer().getNameTag().getId(), data, false);
});
/**
* 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, false);
});
/**
* 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)),
(map, npc, player) ->
{
if(map.isEmpty())
return null;
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))));
return new SetEquipmentPacket(npc.getServerPlayer().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}.
*/
public static final NpcOption<SkinParts[], SkinParts[]> SKIN_PARTS = new NpcOption<>("skin-parts", SkinParts.values(),
skinParts -> skinParts, skinParts -> skinParts,
(skinParts, npc, player) ->
{
WrappedEntityData data = npc.getServerPlayer().getEntityData();
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(17),
(byte) Arrays.stream(skinParts).mapToInt(SkinParts::getValue).sum());
return SetEntityDataPacket.create(npc.getServerPlayer().getId(), data, true);
});
/**
* 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,
(distance, npc, player) -> null);
/**
* NPC option to make the NPC glow with a specific color.
* If null, the glowing effect is removed.
*/
public static final NpcOption<ChatColor, ChatColor> GLOWING = new NpcOption<>("glowing", null,
color -> color, color -> color,
(color, npc, player) ->
{
if(color == null)
{
WrappedEntityData entityData = npc.getServerPlayer().getEntityData();
entityData.set(WrappedEntityData.EntityDataSerializers.BYTE.create(0), (byte) 0);
return SetEntityDataPacket.create(npc.getServerPlayer().getId(), entityData, false);
}
String teamName = npc.getServerPlayer().getName();
boolean modified = WrappedPlayerTeam.exists(player, teamName);
WrappedPlayerTeam wrappedPlayerTeam = WrappedPlayerTeam.create(player, teamName);
wrappedPlayerTeam.setColor(ChatFormat.fromChatColor(color));
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified);
WrappedEntityData entityData = npc.getServerPlayer().getEntityData();
entityData.set(WrappedEntityData.EntityDataSerializers.BYTE.create(0), (byte) 0x40);
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.getServerPlayer().getId(), entityData, true));
});
/**
* 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, npc, player) ->
{
WrappedAttributeInstance instance = npc.getServerPlayer().getAttribute(WrappedAttributeInstance.Attributes.SCALE_HOLDER);
instance.setBaseValue(scale);
return new UpdateAttributesPacket(npc.getServerPlayer().getId(), instance);
}).since(Versions.V1_20_6);
/**
* 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,
(enabled, npc, player) ->
{
if(enabled)
return null;
WrappedArmorStand armorStand = WrappedArmorStand.create(npc.getLocation().getWorld());
armorStand.moveTo(
npc.getLocation().clone().add(0, (npc.getServerPlayer().getBoundingBox().getYSize() * npc.getOption(SCALE)) + 0.3, 0));
PacketWrapper addPacket = armorStand.getAddEntityPacket();
WrappedEntityData data = armorStand.getEntityData();
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(0), (byte) 0x20);
data.set(WrappedEntityData.EntityDataSerializers.OPTIONAL_CHAT_COMPONENT.create(2),
Optional.of(WrappedComponent.create("DISABLED").setFormats(ChatFormat.RED).getHandle()));
data.set(WrappedEntityData.EntityDataSerializers.BOOLEAN.create(3), true);
data.set(WrappedEntityData.EntityDataSerializers.BOOLEAN.create(4), true);
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(15), (byte) 0x10);
npc.toDeleteEntities.add(armorStand.getId());
return new BundlePacket(addPacket, SetEntityDataPacket.create(armorStand.getId(), data, true));
});
private final String path;
private final T defaultValue;
private final Function<T, S> serializer;
private final Function<S, T> deserializer;
private final TriFunction<T, NPC, Player, PacketWrapper> packet;
private Versions since = Versions.V1_17;
/**
* Private constructor to create a new NpcOption.
*
* @param path The configuration path string. Must not be null.
* @param defaultValue The default value for the option. Can 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)
{
this.path = path;
this.defaultValue = defaultValue;
this.serializer = serializer;
this.deserializer = deserializer;
this.packet = packet;
}
/**
* Retrieves all declared {@link NpcOption} constants within this class using reflection.
*
* @return An array of {@link NpcOption} instances. Will not be null.
*/
public static @NotNull NpcOption<?, ?>[] values()
{
List<Field> fields = Arrays.stream(NpcOption.class.getDeclaredFields()).filter(field -> field.getType().equals(NpcOption.class)).toList();
NpcOption<?, ?>[] values = new NpcOption[fields.size()];
for(int i = 0; i < fields.size(); i++)
{
try
{
values[i] = (NpcOption<?, ?>) fields.get(i).get(null);
} catch(IllegalAccessException e)
{
}
}
return values;
}
/**
* Retrieves an {@link NpcOption} instance by its configuration path.
*
* @param path The configuration path string to search for. Must not be null.
* @return An {@link Optional} containing the found {@link NpcOption}, or an empty Optional if no option matches the path.
*/
public static @NotNull Optional<NpcOption<?, ?>> getOption(@NotNull String path)
{
return Arrays.stream(values()).filter(npcOption -> npcOption.getPath().equals(path)).findFirst();
}
/**
* 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.
*/
public @NotNull NpcOption<T, S> since(@NotNull Versions since)
{
this.since = since;
return this;
}
/**
* 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.
*/
public boolean isCompatible()
{
return !Versions.isCurrentVersionSmallerThan(since);
}
/**
* Gets the minimum Minecraft version required for this option.
*
* @return The {@link Versions} instance.
*/
public Versions since()
{
return since;
}
/**
* Gets the default value for this option.
*
* @return The default value, which can be null if defined as such.
*/
public @Nullable T getDefaultValue()
{
return defaultValue;
}
/**
* Gets the configuration path string for this option.
*
* @return The path string. Will not be null.
*/
public @NotNull String getPath()
{
return path;
}
/**
* Serializes the given value (of type T) into its serializable form (type S).
*
* @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.
*/
@SuppressWarnings("unchecked")
public @Nullable S serialize(@Nullable Object var1)
{
try
{
return serializer.apply((T) var1);
} catch(ClassCastException e)
{
throw new RuntimeException(path + " -> " + var1);
}
}
/**
* Deserializes the given value (of type S) back into its usable form (type T).
*
* @param var1 The serialized value to deserialize. Can be null.
* @return The deserialized value. Can be null if the input or deserializer result is null.
*/
public @Nullable T deserialize(@Nullable S var1)
{
return deserializer.apply(var1);
}
/**
* 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.
*/
@SuppressWarnings("unchecked")
public @NotNull Optional<PacketWrapper> getPacket(@Nullable Object object, @NotNull NPC npc, @NotNull Player player)
{
if(packet == null || !isCompatible())
return Optional.empty();
return Optional.ofNullable(packet.apply((T) object, npc, player));
}
}
@@ -0,0 +1,181 @@
package de.eisi05.npc.api.objects;
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.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.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
import java.io.Serial;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* 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.).
* @param signature The signature used to verify the authenticity of the skin data.
*/
public record Skin(@Nullable String name, @NotNull String value, @NotNull String signature) implements Serializable
{
@Serial
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.
*/
private static final Map<UUID, Skin> skinCache = new HashMap<>();
/**
* 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.
* @throws VersionNotFound If the API does not support the current server version.
*/
public static @Nullable Skin fromPlayer(@NotNull Player player)
{
var properties = WrappedServerPlayer.fromPlayer(player).getGameProfile().getProperties().get("textures").iterator();
if(!properties.hasNext())
return null;
var property = properties.next();
if(Versions.getVersion() == Versions.NONE)
throw new VersionNotFound();
return Versions.isCurrentVersionSmallerThan(Versions.V1_20_2) ?
new Skin(player.getName(), Reflections.<String>invokeMethod(property, "getValue").get(),
Reflections.<String>invokeMethod(property, "getSignature").get()) :
new Skin(player.getName(), Reflections.<String>invokeMethod(property, "value").get(),
Reflections.<String>invokeMethod(property, "signature").get());
}
/**
* Fetches a player's skin from Mojang's session server using their UUID.
* This method first checks the local cache (`skinCache`) before making an HTTP request.
* The fetched skin is added to the cache for future use.
*
* @param uuid The UUID of the player whose skin is to be fetched. Must not be {@code null}.
* @return A {@link Skin} object if the skin is successfully fetched or found in cache, otherwise {@code null}.
* @throws VersionNotFound If the API does not support the current server version (though this check might be redundant here).
*/
private static @Nullable Skin fetchSkin(@NotNull UUID uuid)
{
if(skinCache.containsKey(uuid))
return skinCache.get(uuid);
try
{
URL url = URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false").toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setReadTimeout(10000);
try(InputStream is = connection.getInputStream(); Scanner scanner = new Scanner(is))
{
String response = scanner.useDelimiter("\\A").next();
JsonObject json = new JsonParser().parse(response).getAsJsonObject();
String name = json.get("name").getAsString();
JsonArray properties = json.getAsJsonArray("properties");
for(JsonElement prop : properties)
{
JsonObject obj = prop.getAsJsonObject();
String value = obj.get("value").getAsString();
String signature = obj.has("signature") ? obj.get("signature").getAsString() : null;
if(Versions.getVersion() == Versions.NONE)
throw new VersionNotFound();
Skin skin = new Skin(name, value, signature);
skinCache.put(uuid, skin);
return skin;
}
return null;
}
} catch(Exception e)
{
return null;
}
}
/**
* Fetches a player's skin by their username.
* This method first calls Mojang's API to get the player's UUID from their name
* and then uses the UUID to fetch the skin data.
*
* @param name The username of the player whose skin is to be fetched. Must not be {@code null}.
* @return A {@link Skin} object if the skin is successfully fetched, otherwise {@code null}.
*/
private static Skin fetchSkin(@NotNull String name)
{
try
{
URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000);
try(InputStream is = conn.getInputStream(); Scanner scanner = new Scanner(is))
{
String response = scanner.useDelimiter("\\A").next();
JsonObject json = new JsonParser().parse(response).getAsJsonObject();
String id = json.get("id").getAsString();
return fetchSkin(UUID.fromString(id.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5")));
}
} catch(Exception e)
{
return null;
}
}
/**
* Asynchronously fetches a player's skin using their UUID.
* This method wraps the synchronous {@link #fetchSkin(UUID)} call in a {@link CompletableFuture}
* to prevent blocking the main thread.
*
* @param uuid The UUID of the player whose skin is to be fetched. Must not be {@code null}.
* @return A {@link CompletableFuture} that will complete with the {@link Skin} object, or {@code null} if fetching fails.
*/
public static CompletableFuture<Skin> fetchSkinAsync(@NotNull UUID uuid)
{
return CompletableFuture.supplyAsync(() -> fetchSkin(uuid));
}
/**
* Asynchronously fetches a player's skin using their username.
* This method wraps the synchronous {@link #fetchSkin(String)} call in a {@link CompletableFuture}
* to prevent blocking the main thread.
*
* @param name The username of the player whose skin is to be fetched. Must not be {@code null}.
* @return A {@link CompletableFuture} that will complete with the {@link Skin} object, or {@code null} if fetching fails.
*/
public static CompletableFuture<Skin> fetchSkinAsync(@NotNull String name)
{
return CompletableFuture.supplyAsync(() -> fetchSkin(name));
}
}
@@ -0,0 +1,52 @@
package de.eisi05.npc.api.objects;
import de.eisi05.npc.api.NpcApi;
import de.eisi05.npc.api.manager.NpcManager;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
/**
* 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
{
/**
* 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()
{
lookAtTask();
}
/**
* Implements a recurring task that makes NPCs look at nearby players.
* The task runs on a timer defined by {@code NpcApi.config.getLookAtTimer()}.
* NPCs will only look at players within a specified range, which is
* configured via {@link NpcOption#LOOK_AT_PLAYER}.
*/
private static void lookAtTask()
{
new BukkitRunnable()
{
@Override
public void run()
{
NpcManager.getList().forEach(npc ->
{
double range = npc.getOption(NpcOption.LOOK_AT_PLAYER);
if(range <= 0)
return;
npc.getServerPlayer().getBukkitPlayer().getNearbyEntities(range, range, range)
.stream().filter(entity -> entity instanceof Player)
.forEach(entity -> npc.lookAtPlayer((Player) entity));
});
}
}.runTaskTimer(NpcApi.plugin, 0, NpcApi.config.getLookAtTimer());
}
}
@@ -0,0 +1,55 @@
package de.eisi05.npc.api.utils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandles;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* The {@link CallerUtils} class provides utility methods for retrieving information
* about the calling class and method in the current stack trace.
* It leverages {@link StackWalker} for efficient stack traversal and
* caches {@link MethodHandles.Lookup} instances for performance.
*/
public class CallerUtils
{
private static final Map<Class<?>, MethodHandles.Lookup> lookupCache = new ConcurrentHashMap<>();
private static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
/**
* Retrieves the {@link Class} of the immediate caller of the method that invokes this utility method.
* It skips the current method and the direct caller of this utility method to find the actual caller.
*
* @return The {@link Class} object of the caller, or {@code null} if it cannot be determined.
*/
public static @Nullable Class<?> getCallerClass()
{
return WALKER.walk(frames -> frames.skip(2).findFirst().map(StackWalker.StackFrame::getDeclaringClass).orElse(null));
}
/**
* Retrieves the name of the immediate caller method of the method that invokes this utility method.
* It skips the current method and the direct caller of this utility method to find the actual caller's method name.
*
* @return The name of the caller method as a {@link String}, or {@code null} if it cannot be determined.
*/
public static @Nullable String getCallerMethodName()
{
return WALKER.walk(frames -> frames.skip(2).findFirst().map(StackWalker.StackFrame::getMethodName).orElse(null));
}
/**
* Retrieves a {@link MethodHandles.Lookup} instance for the given class.
* This method uses a cache to return existing lookup instances for already processed classes,
* or creates and caches a new one if it's the first time the class is requested.
*
* @param clazz The {@link Class} for which to get the {@link MethodHandles.Lookup} instance. Must not be {@code null}.
* @return A {@link MethodHandles.Lookup} instance associated with the provided class.
*/
public static @NotNull MethodHandles.Lookup getLookup(@NotNull Class<?> clazz)
{
return lookupCache.computeIfAbsent(clazz, c -> MethodHandles.lookup());
}
}
@@ -0,0 +1,200 @@
package de.eisi05.npc.api.utils;
import org.bukkit.Bukkit;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
/**
* The {@link ItemSerializer} class provides utility methods for serializing and deserializing
* Bukkit {@link ItemStack} arrays, {@link PlayerInventory} contents, and generic {@link Inventory}
* contents to and from Base64 encoded strings. This is useful for storing inventory data
* in configurations or databases.
*/
public class ItemSerializer
{
/**
* Serializes the contents of a {@link PlayerInventory} into a Base64 encoded string array.
* The array will contain two elements: the first for the main inventory storage contents,
* and the second for the armor contents plus the off-hand item.
*
* @param playerInventory The {@link PlayerInventory} to serialize. Must not be {@code null}.
* @return A {@code String[]} array containing two Base64 encoded strings:
* index 0 is the main inventory, index 1 is armor and off-hand.
* @throws IllegalStateException If an error occurs during serialization (e.g., I/O error).
*/
public static @NotNull String[] playerInventoryToBase64(@NotNull PlayerInventory playerInventory) throws IllegalStateException
{
String content = itemStackArrayToBase64(playerInventory.getStorageContents());
ItemStack[] items = new ItemStack[playerInventory.getArmorContents().length + 1];
for(int i = 0; i < playerInventory.getArmorContents().length; ++i)
items[i] = playerInventory.getArmorContents()[i];
items[items.length - 1] = playerInventory.getItemInOffHand();
String armor = itemStackArrayToBase64(items);
return new String[]{content, armor};
}
/**
* Serializes a single {@link ItemStack} into a Base64 encoded string.
* This is a convenience method that wraps the item in an array and calls {@link #itemStackArrayToBase64(ItemStack[])}.
*
* @param item The {@link ItemStack} to serialize. Must not be {@code null}.
* @return A Base64 encoded {@link String} representing the item.
* Returns {@code null} if an error occurs during serialization.
*/
public static @NotNull String itemStackToBase64(@NotNull ItemStack item)
{
return itemStackArrayToBase64(new ItemStack[]{item});
}
/**
* Deserializes a single {@link ItemStack} from a Base64 encoded string.
* This method expects the Base64 string to represent a single item.
*
* @param data The Base64 encoded {@link String} representing the item. Must not be {@code null}.
* @return The deserialized {@link ItemStack}, or {@code null} if an error occurs during deserialization
* or if the input data does not represent a valid item.
*/
public static @Nullable ItemStack itemStackFromBase64(@NotNull String data)
{
try
{
return itemStackArrayFromBase64(data)[0];
} catch(Exception e)
{
return null;
}
}
/**
* Serializes an array of {@link ItemStack} objects into a Base64 encoded string.
* This method uses Bukkit's object serialization for {@link ItemStack}s.
*
* @param items An array of {@link ItemStack} objects to serialize. Must not be {@code null}.
* @return A Base64 encoded {@link String} representing the array of items.
* Returns {@code null} if an error occurs during serialization.
*/
public static @Nullable String itemStackArrayToBase64(@NotNull ItemStack[] items)
{
try
{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
dataOutput.writeInt(items.length);
for(ItemStack item : items)
dataOutput.writeObject(item);
dataOutput.close();
return Base64Coder.encodeLines(outputStream.toByteArray());
} catch(Exception e)
{
return null;
}
}
/**
* Serializes the contents of a generic {@link Inventory} into a Base64 encoded string.
* This method is suitable for any type of inventory (e.g., chests, custom inventories).
*
* @param inventory The {@link Inventory} to serialize. Must not be {@code null}.
* @return A Base64 encoded {@link String} representing the inventory contents.
* Returns an empty string if an error occurs during serialization.
*/
public static @NotNull String toBase64(@NotNull Inventory inventory)
{
try
{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
dataOutput.writeInt(inventory.getSize());
for(ItemStack item : inventory.getContents())
dataOutput.writeObject(item);
dataOutput.close();
return Base64Coder.encodeLines(outputStream.toByteArray());
} catch(Exception e)
{
return "";
}
}
/**
* Deserializes two Base64 encoded strings back into two {@link ItemStack} arrays.
* This is typically used for deserializing player inventory content and armor/off-hand.
*
* @param s A {@code String[]} array containing two Base64 encoded strings,
* where {@code s[0]} is the main inventory and {@code s[1]} is armor/off-hand. Must not be {@code null}.
* @return A two-dimensional {@code ItemStack[][]} array, where the first array
* contains the deserialized main inventory items and the second array
* contains the deserialized armor and off-hand items.
*/
public static @NotNull ItemStack[][] doubleInventoryFromBase64(@NotNull String[] s)
{
return new ItemStack[][]{itemStackArrayFromBase64(s[0]), itemStackArrayFromBase64(s[1])};
}
/**
* Deserializes a Base64 encoded string back into a Bukkit {@link Inventory} object.
* The inventory's size and contents are restored from the provided data.
*
* @param data The Base64 encoded {@link String} representing the inventory. Must not be {@code null}.
* @return The deserialized {@link Inventory} object, or {@code null} if an error occurs
* during deserialization or if the input data is invalid.
*/
public static @Nullable Inventory fromBase64(@NotNull String data)
{
try
{
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data));
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
Inventory inventory = Bukkit.getServer().createInventory(null, dataInput.readInt());
for(int i = 0; i < inventory.getSize(); ++i)
inventory.setItem(i, (ItemStack) dataInput.readObject());
dataInput.close();
return inventory;
} catch(Exception e)
{
return null;
}
}
/**
* Deserializes a Base64 encoded string back into an array of {@link ItemStack} objects.
*
* @param data The Base64 encoded {@link String} representing the array of items. Must not be {@code null}.
* @return An array of deserialized {@link ItemStack} objects.
* Returns an empty {@code ItemStack[]} array if an error occurs during deserialization.
*/
public static @NotNull ItemStack[] itemStackArrayFromBase64(@NotNull String data)
{
try
{
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data));
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
ItemStack[] items = new ItemStack[dataInput.readInt()];
for(int i = 0; i < items.length; ++i)
items[i] = (ItemStack) dataInput.readObject();
dataInput.close();
return items;
} catch(Exception e)
{
return new ItemStack[0];
}
}
}
@@ -0,0 +1,164 @@
package de.eisi05.npc.api.utils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
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.
*/
public class ObjectSaver
{
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
* 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}.
* @throws RuntimeException If an {@link IOException} occurs while creating the file.
*/
public ObjectSaver(@NotNull String file)
{
this(new File(file));
}
/**
* 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.
*/
public ObjectSaver(@NotNull File file)
{
file.getParentFile().mkdirs();
this.file = file;
if(!file.exists())
{
try
{
file.createNewFile();
} catch(IOException e)
{
throw new RuntimeException(e);
}
}
}
/**
* 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}.
* @throws IOException If an I/O error occurs during writing.
*/
public <T extends Serializable> void write(T object) throws IOException
{
this.write(object, true);
}
/**
* 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)
* 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.
*/
public <T extends Serializable> void write(T object, boolean append) throws IOException
{
FileOutputStream fileOut = new FileOutputStream(this.file, append);
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
objectOut.writeObject(object);
objectOut.close();
}
/**
* 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}.
* @throws IOException If an I/O error occurs during writing.
*/
public <T extends Serializable> void writeList(@NotNull List<T> object) throws IOException
{
this.writeList(object, true);
}
/**
* 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)
* 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.
*/
public <T extends Serializable> void writeList(@NotNull List<T> object, boolean append) throws IOException
{
FileOutputStream fileOut = new FileOutputStream(this.file, append);
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
objectOut.writeObject(object);
objectOut.close();
}
/**
* 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).
*/
@SuppressWarnings("unchecked")
public <T extends Serializable> @Nullable T read()
{
try
{
FileInputStream fileIn = new FileInputStream(this.file);
ObjectInputStream objectOut = new ObjectInputStream(fileIn);
Object object = objectOut.readObject();
objectOut.close();
return (T) object;
} catch(Exception e)
{
return null;
}
}
/**
* 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.
*/
@SuppressWarnings("unchecked")
public <T extends Serializable> @NotNull List<T> readList()
{
try
{
FileInputStream fileIn = new FileInputStream(this.file);
ObjectInputStream objectOut = new ObjectInputStream(fileIn);
Object object = objectOut.readObject();
objectOut.close();
return (List<T>) object;
} catch(Exception var4)
{
return new ArrayList();
}
}
}
@@ -0,0 +1,155 @@
package de.eisi05.npc.api.utils;
import de.eisi05.npc.api.NpcApi;
import de.eisi05.npc.api.enums.ClickActionType;
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.WrappedServerPlayer;
import de.eisi05.npc.api.wrapper.packets.PacketWrapper;
import de.eisi05.npc.api.wrapper.packets.UseEntityPacketWrapper;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
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.
*/
public class PacketReader
{
private static final Map<UUID, Channel> channels = new HashMap<>();
private static final List<BiConsumer<Player, Object>> readers = new ArrayList<>();
/**
* 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}.
*/
public static void addReader(@NotNull BiConsumer<Player, Object> reader)
{
readers.add(reader);
}
/**
* 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}.
*/
public static void inject(@NotNull Player player)
{
WrappedServerPlayer serverPlayer = WrappedServerPlayer.fromPlayer(player);
Channel channel = serverPlayer.playerConnection().networkManager().channel();
channels.put(player.getUniqueId(), channel);
if(channel.pipeline().get(NpcApi.plugin.getName()) != null)
return;
ChannelDuplexHandler duplexHandler = new ChannelDuplexHandler()
{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
{
checkForPacket(msg, player);
readers.forEach(consumer -> consumer.accept(player, msg));
super.channelRead(ctx, msg);
}
};
if(channel.pipeline().get("packet_handler") == null)
channel.pipeline().addLast(NpcApi.plugin.getName(), duplexHandler);
else
channel.pipeline().addBefore("packet_handler", NpcApi.plugin.getName(), duplexHandler);
}
/**
* 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}.
*/
private static void checkForPacket(@NotNull Object packet, @NotNull Player player)
{
if(!PacketWrapper.PacketHolder.is(packet, PacketWrapper.PacketHolder.class))
return;
if(!PacketWrapper.PacketHolder.is(packet, UseEntityPacketWrapper.class))
return;
UseEntityPacketWrapper useEntityPacketWrapper = PacketWrapper.PacketHolder.wrap(packet, UseEntityPacketWrapper.class);
int id = useEntityPacketWrapper.getId();
NPC npc = NpcManager.getList().stream().filter(npc1 -> npc1.getServerPlayer().getId() == id).findFirst().orElse(null);
if(npc == null)
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);
}
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);
}
/**
* 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}.
*/
public static void uninject(@NotNull Player player)
{
Channel channel = channels.get(player.getUniqueId());
if(channel == null)
return;
if(channel.pipeline().get(NpcApi.plugin.getName()) != null)
channel.pipeline().remove(NpcApi.plugin.getName());
}
/**
* Uninjects the custom packet handler from all currently online players.
* This is typically called during plugin shutdown or reload.
*/
public static void uninjectAll()
{
for(Player player : Bukkit.getOnlinePlayers())
uninject(player);
}
/**
* 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()
{
for(Player player : Bukkit.getOnlinePlayers())
inject(player);
}
}
@@ -0,0 +1,396 @@
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;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
/**
* Utility class providing methods for reflective operations such as loading classes,
* invoking methods, and accessing fields dynamically at runtime.
*/
@SuppressWarnings("unchecked")
public class Reflections
{
/**
* Loads a class by its fully qualified name.
*
* @param path fully qualified class name
* @param <T> type of the class
* @return Optional containing the Class if found, empty otherwise
*/
public static <T> @NotNull Optional<Class<T>> getClass(@NotNull String path)
{
try
{
return Optional.of((Class<T>) Class.forName(path));
} catch(ClassNotFoundException e)
{
return Optional.empty();
}
}
/**
* Instantiates an object of the given class using the constructor that matches the argument types.
*
* @param path fully qualified class name
* @param args constructor arguments
* @param <T> type of the instance
* @return Optional containing the instance if created successfully, empty otherwise
*/
public static <T> @NotNull Optional<T> getInstance(@NotNull String path, @Nullable Object... args)
{
return getClass(path).flatMap(objectClass -> getInstance((Class<T>) objectClass, args));
}
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);
Constructor<?> ctor = clazz.getDeclaredConstructor(argTypes);
ctor.setAccessible(true);
return Optional.of((T) ctor.newInstance(newArgs));
} catch(Exception e)
{
return Optional.empty();
}
}
/**
* Finds a declared method in the specified class that matches the given name and parameter types.
*
* @param clazz the class to search in
* @param name the name of the method
* @param args the arguments whose types will be used to find the method
* @return the matching Method object
* @throws NoSuchMethodException if no matching method is found
*/
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);
while(current != null)
{
for(Method method : current.getDeclaredMethods())
{
if(!method.getName().equals(name))
continue;
Class<?>[] paramTypes = method.getParameterTypes();
boolean isVarArgs = method.isVarArgs();
if(isCompatible(argTypes, paramTypes, isVarArgs))
{
method.setAccessible(true);
return method;
}
}
current = current.getSuperclass();
}
throw new NoSuchMethodException("No compatible method " + name + " found in class " + clazz.getName() + "(" + Arrays.toString(args) + ")");
}
/**
* 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).
*
* @param args The types of the provided arguments.
* @param params The types of the method's parameters.
* @param isVarArgs Whether the method accepts a variable number of arguments (varargs).
* @return {@code true} if the argument types are compatible with the parameter types; {@code false} otherwise.
*/
private static boolean isCompatible(@NotNull Class<?>[] args, @NotNull Class<?>[] params, boolean isVarArgs)
{
if(!isVarArgs)
{
if(args.length != params.length)
return false;
for(int i = 0; i < args.length; i++)
{
if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(args[i])))
return false;
}
return true;
}
if(args.length < params.length - 1)
return false;
for(int i = 0; i < params.length - 1; i++)
{
if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(args[i])))
return false;
}
Class<?> varArgType = Primitives.wrap(params[params.length - 1].getComponentType());
for(int i = params.length - 1; i < args.length; i++)
{
if(!varArgType.isAssignableFrom(Primitives.wrap(args[i])))
return false;
}
return true;
}
/**
* Invokes an instance method on the given object with specified arguments.
*
* @param object the target object
* @param methodName name of the method to invoke
* @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> 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)
{
throw new RuntimeException(e);
}
}
/**
* Invokes a static method of a class given its fully qualified name.
*
* @param classPath fully qualified class name
* @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 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)
{
throw new RuntimeException(e);
}
}
/**
* Finds a declared field in the specified class by its name and makes it accessible.
*
* @param clazz the class to search in
* @param fieldName the name of the field
* @return the Field object with accessibility set to true
* @throws NoSuchFieldException if the field is not found
*/
private static @NotNull Field findField(@NotNull Class<?> clazz, @NotNull String fieldName) throws NoSuchFieldException
{
while(clazz != null)
{
try
{
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch(NoSuchFieldException ignored)
{
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException();
}
/**
* Retrieves the value of a field from an object.
*
* @param object the target object
* @param fieldName name of the field
* @param <T> type of the field value
* @return a ReflectionChain wrapping the field's value
*/
public static <T> @NotNull ReflectionChain<T> getField(@NotNull Object object, @NotNull String fieldName)
{
try
{
Field field = findField(object.getClass(), fieldName);
return new ReflectionChain<>((T) field.get(object));
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Retrieves the value of a static field from a class.
*
* @param clazz the target class
* @param fieldName name of the static field
* @param <T> class type
* @param <V> type of the field value
* @return the value of the static field, or null if inaccessible
*/
public static <T, V> @Nullable V getStaticField(@NotNull Class<T> clazz, @Nullable String fieldName)
{
try
{
Field field = findField(clazz, fieldName);
return (V) field.get(null);
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Retrieves the value of a static field by class name.
*
* @param classPath fully qualified class name
* @param fieldName name of the static field
* @param <T> type of the field value
* @return the value of the static field, or null if inaccessible
*/
public static <T> @Nullable T getStaticField(@NotNull String classPath, @NotNull String fieldName)
{
try
{
Class<?> clazz = Class.forName(classPath);
Field field = findField(clazz, fieldName);
return (T) field.get(null);
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Sets the value of a field on an object.
*
* @param object target object
* @param fieldName name of the field
* @param value new value to set
*/
public static void setField(@NotNull Object object, @NotNull String fieldName, @Nullable Object value)
{
try
{
Field field = findField(object.getClass(), fieldName);
field.set(object, value);
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Sets the value of a static field in a class by class name.
*
* @param classPath fully qualified class name
* @param fieldName name of the static field
* @param value new value to set
*/
public static void setStaticField(@NotNull String classPath, @NotNull String fieldName, @Nullable Object value)
{
try
{
Class<?> clazz = Class.forName(classPath);
Field field = findField(clazz, fieldName);
field.set(null, value);
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Helper class to chain reflection calls on the result of previous reflective operations.
*
* @param <V> the wrapped value type
*/
public static class ReflectionChain<V>
{
private final @Nullable V value;
/**
* Creates a new ReflectionChain wrapping the given value.
*
* @param value the wrapped value may be null
*/
public ReflectionChain(@Nullable V value)
{
this.value = value;
}
/**
* Invokes a method on the wrapped object.
*
* @param methodName name of the method
* @param args method arguments
* @return new ReflectionChain wrapping the method's result, or null if error
*/
public @NotNull ReflectionChain<V> thanInvoke(@NotNull String methodName, @Nullable Object... args)
{
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)
{
throw new RuntimeException(e);
}
}
/**
* Retrieves a field value from the wrapped object.
*
* @param fieldName name of the field
* @return new ReflectionChain wrapping the field's value, or null if error
*/
public @NotNull ReflectionChain<V> thanGetField(@NotNull String fieldName)
{
if(value == null)
return new ReflectionChain<>(null);
try
{
Field field = findField(value.getClass(), fieldName);
return new ReflectionChain<>((V) field.get(value));
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Returns the wrapped value.
*
* @return wrapped value may be null
*/
public @Nullable V get()
{
return value;
}
}
}
@@ -0,0 +1,24 @@
package de.eisi05.npc.api.utils;
/**
* 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)}.
*
* @param <T> the type of the first argument to the function
* @param <U> the type of the second argument to the function
* @param <V> the type of the third argument to the function
* @param <R> the type of the result of the function
*/
@FunctionalInterface
public interface TriFunction<T, U, V, R>
{
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @param v the third function argument
* @return the function result
*/
R apply(T t, U u, V v);
}
@@ -0,0 +1,92 @@
package de.eisi05.npc.api.utils;
import de.eisi05.npc.api.wrapper.objects.WrappedComponent;
import org.bukkit.World;
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.
*/
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.
*
* @param itemStack The Bukkit {@link ItemStack} to convert. Must not be {@code null}.
* @return The NMS {@link Object} representing the {@code ItemStack}.
* @throws java.util.NoSuchElementException If the reflection method is not found or cannot be invoked.
*/
public static @NotNull Object toNmsItemStack(@NotNull ItemStack itemStack)
{
return Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".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.
*
* @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}).
* @throws java.util.NoSuchElementException If the reflection method is not found or cannot be invoked.
*/
public static @NotNull Object getNmsLevel(@NotNull World world)
{
return Reflections.invokeMethod(world, "getHandle").get();
}
/**
* 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.
* @throws java.util.NoSuchElementException If the reflection method is not found or cannot be invoked.
*/
public static @NotNull WrappedComponent fromString(@NotNull String text)
{
return WrappedComponent.fromHandle(
Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".util.CraftChatMessage",
"fromStringOrNull", text).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.
*
* @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.
* @throws java.util.NoSuchElementException If the reflection method is not found or cannot be invoked.
*/
public static @NotNull String toString(@NotNull WrappedComponent component)
{
return (String) Reflections.invokeStaticMethod("org.bukkit.craftbukkit." + Versions.getVersion().getPath() + ".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.
*
* @param o The object to cast. Can be {@code null}.
* @param <T> The target type to which the object will be cast.
* @return The object cast to the specified type, or {@code null} if the input object was {@code null}.
*/
@SuppressWarnings("unchecked")
public static <T> @Nullable T unsafeCast(@Nullable Object o)
{
return (T) o;
}
}
@@ -0,0 +1,193 @@
package de.eisi05.npc.api.utils;
import de.eisi05.npc.api.NpcApi;
import de.eisi05.npc.api.utils.exceptions.VersionNotFound;
import de.eisi05.npc.api.wrapper.Mapping;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
/**
* The {@link Versions} enum represents the supported Minecraft server versions
* and provides utility methods for working with version-specific paths and comparisons.
* It helps in adapting the plugin's functionality to different server environments.
*/
public enum Versions
{
/**
* Represents an unknown or unsupported version.
*/
NONE(""),
/**
* Minecraft 1.17 version.
*/
V1_17("v1_17_R1"),
/**
* Minecraft 1.18 version.
*/
V1_18("v1_18_R1"),
/**
* Minecraft 1.18.2 version.
*/
V1_18_2("v1_18_R2"),
/**
* Minecraft 1.19 version.
*/
V1_19("v1_19_R1"),
/**
* Minecraft 1.19.1 version.
*/
V1_19_1("v1_19_R1"),
/**
* Minecraft 1.19.3 version.
*/
V1_19_3("v1_19_R2"),
/**
* Minecraft 1.19.4 version.
*/
V1_19_4("v1_19_R3"),
/**
* Minecraft 1.20 version.
*/
V1_20("v1_20_R1"),
/**
* Minecraft 1.20.2 version.
*/
V1_20_2("v1_20_R2"),
/**
* Minecraft 1.20.4 version.
*/
V1_20_4("v1_20_R3"),
/**
* Minecraft 1.20.6 version.
*/
V1_20_6("v1_20_R4"),
/**
* Minecraft 1.21 version.
*/
V1_21("v1_21_R1"),
/**
* Minecraft 1.21.2 version.
*/
V1_21_2("v1_21_R2"),
/**
* Minecraft 1.21.4 version.
*/
V1_21_4("v1_21_R3"),
/**
* Minecraft 1.21.5 version.
*/
V1_21_5("v1_21_R4");
/**
* Caches the determined current server version to avoid repeated lookups.
*/
private static Versions VERSION;
/**
* The NMS (Net Minecraft Server) path component corresponding to this version.
* For example, "v1_17_R1" for Minecraft 1.17.
*/
private final String path;
/**
* Constructs a {@code Versions} enum entry with the specified NMS path.
*
* @param path The NMS path string for this version. Must not be {@code null}.
*/
Versions(@NotNull String path)
{
this.path = path;
}
/**
* Determines and returns the current Minecraft server version based on the Bukkit server's package name.
* The determined version is cached for later calls. If the version is not recognized,
* the plugin will be disabled and a {@link VersionNotFound} exception will be thrown.
*
* @return The {@link Versions} enum entry corresponding to the current server version. Must not be {@code null}.
* @throws VersionNotFound If the current server version cannot be determined or is not supported.
*/
public static @NotNull Versions getVersion()
{
if(VERSION != null)
return VERSION;
String nmsVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
return VERSION = Arrays.stream(Versions.values())
.filter(v -> v.path.equalsIgnoreCase(nmsVersion))
.reduce((a, b) -> Bukkit.getBukkitVersion().startsWith("1.19.1") ? Versions.V1_19_1 : Versions.V1_19)
.orElseThrow(() ->
{
Bukkit.getPluginManager().disablePlugin(NpcApi.plugin);
return new VersionNotFound(nmsVersion);
});
}
/**
* Returns an array of {@link Versions} enum entries that fall inclusively between
* two specified versions (based on their ordinal values).
* The {@code NONE} version is excluded from the result.
*
* @param versions1 The starting version (inclusive). Must not be {@code null}.
* @param versions2 The ending version (inclusive). Must not be {@code null}.
* @return An array of {@link Versions} enum entries within the specified range. Must not be {@code null}.
*/
private static @NotNull Versions[] getVersionBetween(@NotNull Versions versions1, @NotNull Versions versions2)
{
return Arrays.stream(values())
.filter(v -> v != Versions.NONE)
.filter(v -> v.ordinal() >= versions1.ordinal() && v.ordinal() <= versions2.ordinal())
.toArray(Versions[]::new);
}
/**
* Checks if the current server version is numerically smaller than a specified version.
* This comparison is based on the ordinal value of the enum entries.
*
* @param versions The version to compare against. Must not be {@code null}.
* @return {@code true} if the current version is smaller, {@code false} otherwise.
*/
public static boolean isCurrentVersionSmallerThan(@NotNull Versions versions)
{
return getVersion().ordinal() < versions.ordinal();
}
/**
* Checks if the current server version is contained within a given {@link Mapping}.
* A mapping can specify either a fixed version or a range of versions.
*
* @param mapping The {@link Mapping} to check against. Must not be {@code null}.
* @return {@code true} if the current version matches the mapping, {@code false} otherwise.
*/
public static boolean containsCurrentVersion(@NotNull Mapping mapping)
{
if(mapping.fixed().value() != NONE)
return mapping.fixed().value() == getVersion();
return Arrays.stream(getVersionBetween(mapping.range().from(), mapping.range().to())).anyMatch(v -> v == getVersion());
}
/**
* Returns the NMS (Net Minecraft Server) path component associated with this version.
*
* @return The NMS path as a {@link String}. Must not be {@code null}.
*/
public @NotNull String getPath()
{
return path;
}
/**
* Returns the name of the version, with underscores replaced by dots for better readability.
* For example, {@code V1_17} becomes "V1.17".
*
* @return The formatted name of the version as a {@link String}. Must not be {@code null}.
*/
public @NotNull String getName()
{
return name().replace("_", ".");
}
}
@@ -0,0 +1,57 @@
package de.eisi05.npc.api.utils.exceptions;
import de.eisi05.npc.api.utils.Versions;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Method;
/**
* An exception thrown when a specific version of the server is not found or supported
* for a particular method or class, indicating incompatibility with the current
* Bukkit/Minecraft version. This is a {@link RuntimeException}, meaning it is
* an unchecked exception and does not need to be explicitly caught.
*/
public class VersionNotFound extends RuntimeException
{
/**
* Constructs a new {@code VersionNotFound} exception with a detail message
* indicating that the current server version is not found for a specific method.
*
* @param method The {@link Method} for which the version was not found. Must not be {@code null}.
*/
public VersionNotFound(@NotNull Method method)
{
super("Version " + Versions.getVersion().name() + " not found for " + method.getName() + " in " + method.getDeclaringClass().getName());
}
/**
* Constructs a new {@code VersionNotFound} exception with a detail message
* indicating that the current server version is not found for a specific class.
*
* @param clazz The {@link Class} for which the version was not found. Must not be {@code null}.
*/
public VersionNotFound(@NotNull Class<?> clazz)
{
super("Version " + Versions.getVersion().name() + " not found for " + clazz.getName());
}
/**
* Constructs a new {@code VersionNotFound} exception with no detail message.
* This constructor can be used when the specific context of the version not found
* is not immediately available or required.
*/
public VersionNotFound()
{
super();
}
/**
* Constructs a new {@code VersionNotFound} exception with the specified detail message.
*
* @param message The detail message (which is saved for later retrieval by the {@link Throwable#getMessage()} method). Must not be {@code null}.
*/
public VersionNotFound(@NotNull String message)
{
super(message);
}
}
@@ -0,0 +1,21 @@
package de.eisi05.npc.api.wrapper;
import org.jetbrains.annotations.NotNull;
/**
* The {@link HandleHolder} interface defines a contract for classes that wrap
* an underlying "handle" object, typically a Net Minecraft Server (NMS) object.
* This interface allows for a standardized way to access the NMS instance
* that a wrapper object represents.
*/
public interface HandleHolder
{
/**
* Retrieves the underlying "handle" object.
* This method is used to access the NMS (Net Minecraft Server) instance
* that this object wraps.
*
* @return The underlying handle object. Must not be {@code null}.
*/
@NotNull Object getHandle();
}
@@ -0,0 +1,103 @@
package de.eisi05.npc.api.wrapper;
import de.eisi05.npc.api.utils.Versions;
import org.jetbrains.annotations.NotNull;
import java.lang.annotation.*;
/**
* The {@link Mapping} annotation is used to define version-specific paths or behaviors
* for methods, fields, constructors, or types. It allows specifying either a fixed
* version or a range of versions for which a particular mapping is valid.
* This annotation is {@link Repeatable}, meaning multiple {@code @Mapping} annotations
* can be applied to the same element.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Repeatable(Mapping.WrapList.class)
public @interface Mapping
{
/**
* Specifies a fixed {@link Versions} enum entry for which this mapping is valid.
* If set to {@link Versions#NONE}, the mapping is considered to be defined by a range instead.
*
* @return The fixed version for this mapping. Defaults to {@link Versions#NONE}.
*/
@NotNull Fixed fixed() default @Fixed(Versions.NONE);
/**
* Specifies a range of {@link Versions} enum entries for which this mapping is valid.
* This is used when the mapping applies to a continuous set of versions.
* If both {@code fixed()} and {@code range()} are specified, {@code fixed()} takes precedence
* if its value is not {@link Versions#NONE}.
*
* @return The version range for this mapping. Defaults to a range from {@link Versions#NONE} to {@link Versions#NONE}.
*/
@NotNull Range range() default @Range(from = Versions.NONE, to = Versions.NONE);
/**
* The version-specific path or string associated with this mapping.
* This could be a class name, a method name, a field name, or any other string
* that changes across different Minecraft versions.
*
* @return The path string for this mapping. Must not be {@code null}.
*/
@NotNull String path();
/**
* A container annotation for repeatable {@link Mapping} annotations.
* Java automatically uses this when multiple {@code @Mapping} annotations
* are applied to the same program element.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@interface WrapList
{
/**
* The array of {@link Mapping} annotations.
*
* @return An array of {@link Mapping} instances. Must not be {@code null}.
*/
@NotNull Mapping[] value();
}
/**
* Defines a fixed {@link Versions} enum entry for a {@link Mapping} annotation.
* This nested annotation is used to specify that a mapping is valid for one particular version.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@interface Fixed
{
/**
* The specific {@link Versions} enum entry for which the mapping is valid.
*
* @return The fixed version. Must not be {@code null}.
*/
@NotNull Versions value();
}
/**
* Defines a range of {@link Versions} enum entries for a {@link Mapping} annotation.
* This nested annotation is used to specify that a mapping is valid for a continuous
* set of versions.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@interface Range
{
/**
* The starting {@link Versions} enum entry of the range (inclusive).
*
* @return The starting version of the range. Must not be {@code null}.
*/
@NotNull Versions from();
/**
* The ending {@link Versions} enum entry of the range (inclusive).
*
* @return The ending version of the range. Must not be {@code null}.
*/
@NotNull Versions to();
}
}
@@ -0,0 +1,618 @@
package de.eisi05.npc.api.wrapper;
import com.google.common.primitives.Primitives;
import de.eisi05.npc.api.utils.CallerUtils;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.utils.exceptions.VersionNotFound;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public abstract class Wrapper implements HandleHolder
{
private static final Map<String, Class<?>> classCache = new ConcurrentHashMap<>();
private static final Map<Class<?>, Constructor<?>> wrapperConstructorCache = new ConcurrentHashMap<>();
private static final Map<String, Constructor<?>> instanceConstructorCache = new ConcurrentHashMap<>();
private static final Map<String, MethodHandle> methodCache = new ConcurrentHashMap<>();
private static final Map<String, Field> fieldCache = new ConcurrentHashMap<>();
private static final Map<String, MethodHandle> fieldGetterCache = new ConcurrentHashMap<>();
private static final Map<String, MethodHandle> fieldSetterCache = new ConcurrentHashMap<>();
private static final Map<Class<?>, Map<String, Field>> enumFieldCache = new ConcurrentHashMap<>();
protected final Object handle;
public Wrapper(Object handle)
{
this.handle = handle;
}
public static Object[] checkArgs(Object... args)
{
return Arrays.stream(args)
.map(arg -> (arg instanceof HandleHolder h) ? h.getHandle() : arg)
.toArray();
}
protected static Class<?> getTargetClass(Mapping mapping)
{
return classCache.computeIfAbsent(mapping.path(), name ->
{
try
{
return Class.forName(name);
} catch(ClassNotFoundException e)
{
return null;
}
});
}
protected static Constructor<?> findConstructor(Class<?> targetClass, Object[] args)
{
return Arrays.stream(targetClass.getDeclaredConstructors())
.filter(ctor ->
{
Class<?>[] params = ctor.getParameterTypes();
boolean varArgs = ctor.isVarArgs();
if((!varArgs && params.length != args.length) ||
(varArgs && args.length < params.length - 1))
return false;
for(int i = 0; i < params.length; i++)
{
if(i >= args.length)
return false;
Object arg = args[i];
if(arg == null)
continue;
Class<?> actual = arg.getClass();
if(actual.isAnonymousClass() || actual.isSynthetic())
{
Class<?>[] interfaces = actual.getInterfaces();
actual = interfaces.length > 0 ? interfaces[0] : actual.getSuperclass();
}
if(varArgs && i == params.length - 1)
{
Class<?> comp = params[i].getComponentType();
if(!Primitives.wrap(comp).isAssignableFrom(Primitives.wrap(actual)) && !actual.isArray())
return false;
}
else
{
if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(actual)))
return false;
}
}
return true;
})
.findFirst()
.orElse(null);
}
protected static Object createInstance(Class<? extends Wrapper> wrapperClass, Object... args)
{
Object[] checkedArgs = checkArgs(args);
try
{
Mapping[] annotations = wrapperClass.getAnnotationsByType(Mapping.class);
for(Mapping mapping : annotations)
{
if(!Versions.containsCurrentVersion(mapping))
continue;
Class<?> targetClass = getTargetClass(mapping);
String cacheKey = targetClass.getName() +
Arrays.toString(Arrays.stream(checkedArgs)
.map(a -> a == null ? "null" : a.getClass().getName())
.toArray());
Constructor<?> constructor = instanceConstructorCache.computeIfAbsent(cacheKey, k -> findConstructor(targetClass, checkedArgs));
if(constructor == null)
throw new NoSuchMethodException("No matching constructor found for: " + targetClass.getName() + "(" +
Arrays.toString(checkedArgs) + ")");
Object[] realArgs;
if(constructor.isVarArgs())
{
int fixedCount = constructor.getParameterCount() - 1;
Class<?> varArgType = constructor.getParameterTypes()[fixedCount].getComponentType();
Object varArgsArray = Array.newInstance(varArgType, args.length - fixedCount);
for(int i = 0; i < Array.getLength(varArgsArray); i++)
Array.set(varArgsArray, i, checkedArgs[fixedCount + i]);
realArgs = new Object[constructor.getParameterCount()];
System.arraycopy(checkedArgs, 0, realArgs, 0, fixedCount);
realArgs[fixedCount] = varArgsArray;
}
else
realArgs = checkedArgs;
constructor.setAccessible(true);
return constructor.newInstance(realArgs);
}
throw new VersionNotFound(wrapperClass);
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
protected static <T extends Wrapper> T createWrappedInstance(Class<T> wrapperClass, Object... args)
{
try
{
Constructor<?> cached = wrapperConstructorCache.computeIfAbsent(wrapperClass, clazz ->
{
try
{
Constructor<?> cons = clazz.getDeclaredConstructor(Object.class);
cons.setAccessible(true);
return cons;
} catch(Exception e)
{
throw new RuntimeException(e);
}
});
return wrapperClass.cast(cached.newInstance(createInstance(wrapperClass, args)));
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
protected static <T> Optional<T> getStaticWrappedFieldValue(String fieldName)
{
try
{
Class<?> callerClass = CallerUtils.getCallerClass();
Field callerField = callerClass.getDeclaredField(fieldName);
Mapping[] fieldAnnotations = callerField.getAnnotationsByType(Mapping.class);
Mapping[] classAnnotations = callerClass.getAnnotationsByType(Mapping.class);
if(classAnnotations.length == 0)
throw new IllegalStateException("Missing @WrapData on class: " + callerClass.getName());
Mapping fieldAnnotation = Arrays.stream(fieldAnnotations).filter(Versions::containsCurrentVersion).findFirst()
.orElseThrow(() -> new VersionNotFound(callerClass.getName() + " -> " + callerField.getName()));
for(Mapping classData : classAnnotations)
{
if(!Versions.containsCurrentVersion(classData))
continue;
String key = classData.path() + "#" + fieldAnnotation.path();
Field targetField = fieldCache.computeIfAbsent(key, k ->
{
try
{
Class<?> targetClass = getTargetClass(classData);
Field f = targetClass.getDeclaredField(fieldAnnotation.path());
f.setAccessible(true);
return f;
} catch(Exception e)
{
throw new RuntimeException(e);
}
});
return Optional.ofNullable((T) targetField.get(null));
}
return Optional.empty();
} catch(Exception e)
{
return Optional.empty();
}
}
@SuppressWarnings("unchecked")
public static <T> T invokeStaticWrappedMethod(Object... args)
{
final Object[] newArgs = checkArgs(args);
try
{
String callingMethodName = CallerUtils.getCallerMethodName();
Class<?> callerClass = CallerUtils.getCallerClass();
Class<?> targetClass = getTargetClass(callerClass.getAnnotation(Mapping.class));
Method callingMethod = null;
Class<?> clazz = callerClass;
while(callingMethod == null && clazz != null)
{
callingMethod = Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> m.getName().equals(callingMethodName))
.findFirst()
.orElse(null);
clazz = clazz.getSuperclass();
}
if(callingMethod == null)
throw new NoSuchElementException("No value present -> " + callingMethodName);
Mapping[] annotations = callingMethod.getAnnotationsByType(Mapping.class);
MethodHandles.Lookup lookup = CallerUtils.getLookup(targetClass);
for(Mapping mapping : annotations)
{
if(!Versions.containsCurrentVersion(mapping))
continue;
String methodPath = mapping.path();
String key = targetClass.getName() + "#" + methodPath +
Arrays.toString(Arrays.stream(newArgs)
.map(a -> a == null ? "null" : a.getClass().getName()).toArray());
MethodHandle handle = methodCache.computeIfAbsent(key, k ->
{
try
{
Method method = Arrays.stream(targetClass.getMethods())
.filter(m -> m.getName().equals(methodPath))
.filter(m ->
{
Class<?>[] params = m.getParameterTypes();
if(params.length != newArgs.length)
return false;
for(int i = 0; i < params.length; i++)
{
if(newArgs[i] == null)
continue;
if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(newArgs[i].getClass())))
return false;
}
return true;
})
.findFirst()
.orElseThrow(() -> new NoSuchMethodException(methodPath));
method.setAccessible(true);
return lookup.unreflect(method);
} catch(Exception e)
{
throw new RuntimeException(e);
}
});
return (T) handle.invokeWithArguments(newArgs);
}
throw new VersionNotFound(callingMethod);
} catch(Throwable e)
{
throw new RuntimeException(e);
}
}
protected String getPath()
{
String callingMethodName = CallerUtils.getCallerMethodName();
Method callingMethod = Arrays.stream(getClass().getMethods())
.filter(m -> m.getName().equals(callingMethodName))
.findFirst()
.orElseThrow(() -> new RuntimeException(callingMethodName + " -> " + getClass()));
Mapping[] annotations = callingMethod.getAnnotationsByType(Mapping.class);
return Arrays.stream(annotations).filter(Versions::containsCurrentVersion).findFirst()
.orElseThrow(() -> new VersionNotFound(callingMethod)).path();
}
@SuppressWarnings("unchecked")
protected <T> T invokeWrappedMethod(Object... args)
{
final Object[] newArgs = checkArgs(args);
try
{
String callingMethodName = CallerUtils.getCallerMethodName();
Method callingMethod = null;
Class<?> clazz = getClass();
while(callingMethod == null && clazz != null)
{
callingMethod = Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> m.getName().equals(callingMethodName))
.findFirst()
.orElse(null);
clazz = clazz.getSuperclass();
}
if(callingMethod == null)
throw new NoSuchElementException("No value present -> " + callingMethodName);
Mapping[] annotations = callingMethod.getAnnotationsByType(Mapping.class);
MethodHandles.Lookup lookup = CallerUtils.getLookup(getHandle().getClass());
for(Mapping mapping : annotations)
{
if(!Versions.containsCurrentVersion(mapping))
continue;
String methodPath = mapping.path();
String key = getHandle().getClass().getName() + "#" + methodPath +
Arrays.toString(Arrays.stream(newArgs).map(a -> a == null ? "null" : a.getClass().getName()).toArray());
MethodHandle unboundHandle = methodCache.computeIfAbsent(key, k ->
{
try
{
Method method = Arrays.stream(getHandle().getClass().getMethods())
.filter(m -> m.getName().equals(methodPath))
.filter(m ->
{
Class<?>[] params = m.getParameterTypes();
if(params.length != newArgs.length)
return false;
for(int i = 0; i < params.length; i++)
{
if(newArgs[i] == null)
continue;
if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(newArgs[i].getClass())))
return false;
}
return true;
})
.findFirst()
.orElseThrow(() -> new RuntimeException(new NoSuchMethodException(
methodPath + "(" + Arrays.stream(newArgs).map(o -> o.getClass().toString()).toList() + ")")));
method.setAccessible(true);
return lookup.unreflect(method);
} catch(Exception e)
{
throw new RuntimeException(e);
}
});
MethodHandle boundHandle = unboundHandle.bindTo(getHandle());
return (T) boundHandle.invokeWithArguments(newArgs);
}
throw new VersionNotFound(callingMethod);
} catch(Throwable e)
{
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
protected <T> T getWrappedFieldValue()
{
try
{
String callingMethodName = CallerUtils.getCallerMethodName();
Method callingMethod = null;
Class<?> clazz = getClass();
while(callingMethod == null && clazz != null)
{
callingMethod = Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> m.getName().equals(callingMethodName))
.findFirst()
.orElse(null);
clazz = clazz.getSuperclass();
}
if(callingMethod == null)
throw new NoSuchElementException("No value present -> " + callingMethodName);
Mapping[] annotations = callingMethod.getAnnotationsByType(Mapping.class);
for(Mapping mapping : annotations)
{
if(!Versions.containsCurrentVersion(mapping))
continue;
String fieldName = mapping.path();
String key = getHandle().getClass().getName() + "#" + fieldName;
MethodHandle getter = fieldGetterCache.computeIfAbsent(key, k ->
{
try
{
Field f = null;
Class<?> c = getHandle().getClass();
while(c != null)
{
try
{
f = c.getDeclaredField(fieldName);
if(!Modifier.isStatic(f.getModifiers()))
break;
f = null;
} catch(NoSuchFieldException ignored) {}
c = c.getSuperclass();
}
if(f == null)
throw new NoSuchFieldException(fieldName);
f.setAccessible(true);
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(f.getDeclaringClass(), MethodHandles.lookup());
return lookup.findGetter(f.getDeclaringClass(), fieldName, f.getType());
} catch(NoSuchFieldException | IllegalAccessException e)
{
throw new RuntimeException(e);
}
});
return (T) getter.invoke(getHandle());
}
throw new VersionNotFound(callingMethod);
} catch(Throwable e)
{
throw new RuntimeException(e);
}
}
protected <T> void setWrappedFieldValue(T value)
{
try
{
String callingMethodName = CallerUtils.getCallerMethodName();
Method callingMethod = null;
Class<?> clazz = getClass();
while(callingMethod == null && clazz != null)
{
callingMethod = Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> m.getName().equals(callingMethodName))
.findFirst()
.orElse(null);
clazz = clazz.getSuperclass();
}
if(callingMethod == null)
throw new NoSuchElementException("No value present -> " + callingMethodName);
Mapping[] annotations = callingMethod.getAnnotationsByType(Mapping.class);
for(Mapping mapping : annotations)
{
if(!Versions.containsCurrentVersion(mapping))
continue;
String fieldName = mapping.path();
String key = getHandle().getClass().getName() + "#" + fieldName;
MethodHandle setter = fieldSetterCache.computeIfAbsent(key, k ->
{
try
{
Field f = null;
Class<?> c = getHandle().getClass();
while(c != null)
{
try
{
f = c.getDeclaredField(fieldName);
if(!Modifier.isStatic(f.getModifiers()))
break;
f = null;
} catch(NoSuchFieldException ignored) {}
c = c.getSuperclass();
}
if(f == null)
throw new NoSuchFieldException(fieldName);
f.setAccessible(true);
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(f.getDeclaringClass(), MethodHandles.lookup());
return lookup.findSetter(getHandle().getClass(), fieldName, f.getType());
} catch(NoSuchFieldException | IllegalAccessException e)
{
throw new RuntimeException(e);
}
});
setter.invoke(getHandle(), value);
return;
}
throw new VersionNotFound(callingMethod);
} catch(Throwable e)
{
throw new RuntimeException(e);
}
}
@Override
public @NotNull Object getHandle()
{
return handle;
}
public interface EnumWrapper extends HandleHolder
{
@SuppressWarnings("unchecked")
default <T extends Enum<T> & EnumWrapper, V extends Enum<?>> V cast(T object)
{
Class<?> enumClass = object.getClass();
Mapping[] classData = enumClass.getAnnotationsByType(Mapping.class);
String classPath = Arrays.stream(classData)
.filter(Versions::containsCurrentVersion)
.map(Mapping::path)
.findFirst()
.orElseThrow(() -> new VersionNotFound(enumClass));
Class<?> targetEnum;
try
{
targetEnum = Class.forName(classPath);
} catch(ClassNotFoundException e)
{
throw new RuntimeException(e);
}
Map<String, Field> fields = enumFieldCache.computeIfAbsent(enumClass, cls ->
{
Map<String, Field> map = new ConcurrentHashMap<>();
for(Field f : cls.getDeclaredFields())
{
for(Mapping ann : f.getAnnotationsByType(Mapping.class))
{
if(!Versions.containsCurrentVersion(ann))
continue;
f.setAccessible(true);
map.put(f.getName(), f);
}
}
return map;
});
try
{
Field sourceField = fields.get(object.name());
if(sourceField == null)
throw new VersionNotFound(enumClass);
Mapping[] fieldData = sourceField.getAnnotationsByType(Mapping.class);
String fieldName = Arrays.stream(fieldData)
.filter(Versions::containsCurrentVersion)
.map(Mapping::path)
.findFirst()
.orElseThrow(() -> new VersionNotFound(enumClass));
Field targetField = targetEnum.getDeclaredField(fieldName);
targetField.setAccessible(true);
return (V) targetField.get(null);
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
}
}
@@ -0,0 +1,108 @@
package de.eisi05.npc.api.wrapper.enums;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import org.bukkit.ChatColor;
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_5), path = "net.minecraft.EnumChatFormat")
public enum ChatFormat implements Wrapper.EnumWrapper, Serializable
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
BLACK(ChatColor.BLACK),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
DARK_BLUE(ChatColor.DARK_BLUE),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "c")
DARK_GREEN(ChatColor.DARK_GREEN),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "d")
DARK_AQUA(ChatColor.DARK_AQUA),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "e")
DARK_RED(ChatColor.DARK_RED),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "f")
DARK_PURPLE(ChatColor.DARK_PURPLE),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "g")
GOLD(ChatColor.GOLD),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "h")
GRAY(ChatColor.GRAY),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "i")
DARK_GRAY(ChatColor.DARK_GRAY),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "j")
BLUE(ChatColor.BLUE),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "k")
GREEN(ChatColor.GREEN),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "l")
AQUA(ChatColor.AQUA),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "m")
RED(ChatColor.RED),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "n")
LIGHT_PURPLE(ChatColor.LIGHT_PURPLE),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "o")
YELLOW(ChatColor.YELLOW),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "p")
WHITE(ChatColor.WHITE),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "q")
OBFUSCATED(ChatColor.MAGIC),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "r")
BOLD(ChatColor.BOLD),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "s")
STRIKETHROUGH(ChatColor.STRIKETHROUGH),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "t")
UNDERLINE(ChatColor.UNDERLINE),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "u")
ITALIC(ChatColor.ITALIC),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "v")
RESET(ChatColor.RESET);
private final ChatColor color;
ChatFormat(@NotNull ChatColor color)
{
this.color = color;
}
public static @Nullable ChatFormat fromChatColor(@NotNull ChatColor color)
{
for(ChatFormat format : ChatFormat.values())
{
if(format.color.equals(color))
return format;
}
return null;
}
public ChatColor getBukkitColor()
{
return color;
}
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
@@ -0,0 +1,37 @@
package de.eisi05.npc.api.wrapper.enums;
import de.eisi05.npc.api.utils.Versions;
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_5), path = "net.minecraft.world.entity.EnumItemSlot")
public enum EquipmentSlot implements Wrapper.EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
MAINHAND,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
OFFHAND,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "c")
FEET,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "d")
LEGS,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "e")
CHEST,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "f")
HEAD,
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), path = "g")
BODY;
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
@@ -0,0 +1,22 @@
package de.eisi05.npc.api.wrapper.enums;
import de.eisi05.npc.api.utils.Versions;
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_5), path = "net.minecraft.world.EnumHand")
public enum InteractionHand implements Wrapper.EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
MAIN_HAND,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
OFF_HAND;
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
@@ -0,0 +1,131 @@
package de.eisi05.npc.api.wrapper.enums;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.world.entity.EntityPose")
public enum Pose implements Wrapper.EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
STANDING(getBukkit("STANDING"), Material.ARMOR_STAND),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
FALL_FLYING(getBukkit("FALL_FLYING"), Material.FEATHER),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "c")
SLEEPING(getBukkit("SLEEPING"), Material.RED_BED),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "d")
SWIMMING(getBukkit("SWIMMING"), Material.WATER_BUCKET),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "e")
SPIN_ATTACK(getBukkit("SPIN_ATTACK"), Material.TRIDENT),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "f")
CROUCHING(getBukkit("SNEAKING"), Material.LEATHER_BOOTS),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "g")
LONG_JUMPING(getBukkit("LONG_JUMPING"), null),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "h")
DYING(getBukkit("DYING"), Material.ROTTEN_FLESH),
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_5), path = "i")
CROAKING(getBukkit("CROAKING"), null),
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_5), path = "j")
USING_TONGUE(getBukkit("USING_TONGUE"), null),
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), path = "k")
SITTING(getBukkit("SITTING"), null),
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), 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_5), 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_5), 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_5), 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_5), path = "p")
SLIDING(getBukkit("SLIDING"), null),
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_5), path = "q")
SHOOTING(getBukkit("SHOOTING"), null),
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_5), path = "r")
INHALING(getBukkit("INHALING"), null);
private final org.bukkit.entity.Pose bukkitPose;
private final Material icon;
Pose(@Nullable org.bukkit.entity.Pose bukkitPose, Material icon)
{
this.bukkitPose = bukkitPose;
this.icon = icon;
}
public static @Nullable Pose fromBukkit(@NotNull org.bukkit.entity.Pose bukkitPose)
{
for(Pose pose : Pose.values())
{
if(pose.bukkitPose == null)
continue;
if(pose.bukkitPose == bukkitPose)
return pose;
}
return null;
}
private static @Nullable org.bukkit.entity.Pose getBukkit(@NotNull String pose)
{
try
{
return org.bukkit.entity.Pose.valueOf(pose);
} catch(IllegalArgumentException e)
{
return null;
}
}
public static @NotNull Pose[] getValues()
{
return Arrays.stream(values()).filter(pose -> pose.bukkitPose != null).toArray(Pose[]::new);
}
public org.bukkit.entity.Pose toBukkit()
{
return bukkitPose;
}
public boolean isForPlayer()
{
return icon != null;
}
public Material getIcon()
{
return icon;
}
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
@@ -0,0 +1,35 @@
package de.eisi05.npc.api.wrapper.objects;
import de.eisi05.npc.api.utils.Reflections;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import org.bukkit.World;
import org.bukkit.entity.ArmorStand;
import java.util.Optional;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.world.entity.decoration.EntityArmorStand")
public class WrappedArmorStand extends WrappedEntity<ArmorStand>
{
private WrappedArmorStand(Object handle)
{
super(handle);
}
public static WrappedArmorStand create(World world)
{
return new WrappedArmorStand(
createInstance(WrappedArmorStand.class, EntityTypes.ARMOR_STAND, Reflections.invokeMethod(world, "getHandle").get()));
}
public static WrappedEntityData applyData(WrappedEntity<?> entity, WrappedComponent component)
{
WrappedEntityData data = entity.getEntityData();
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(0), (byte) 0x20);
data.set(WrappedEntityData.EntityDataSerializers.OPTIONAL_CHAT_COMPONENT.create(2), Optional.of(component.getHandle()));
data.set(WrappedEntityData.EntityDataSerializers.BOOLEAN.create(4), true);
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(15), (byte) 0x10);
return data;
}
}
@@ -0,0 +1,42 @@
package de.eisi05.npc.api.wrapper.objects;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
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_5),
path = "net.minecraft.world.entity.ai.attributes.AttributeModifiable")
public class WrappedAttributeInstance extends Wrapper
{
WrappedAttributeInstance(Object handle)
{
super(handle);
}
public WrappedAttributeInstance(Object holder, Consumer<?> consumer)
{
super(createInstance(WrappedAttributeInstance.class, holder, consumer));
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), path = "a")
public void setBaseValue(double value)
{
invokeWrappedMethod(value);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5),
path = "net.minecraft.world.entity.ai.attributes.GenericAttributes")
public static class Attributes extends Wrapper
{
@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")
public static final Object SCALE_HOLDER = getStaticWrappedFieldValue("SCALE_HOLDER").orElse(null);
public Attributes()
{
super(null);
}
}
}
@@ -0,0 +1,21 @@
package de.eisi05.npc.api.wrapper.objects;
import de.eisi05.npc.api.utils.Versions;
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_5), path = "net.minecraft.server.level.ClientInformation")
public class WrappedClientInformation extends Wrapper
{
private WrappedClientInformation(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_5), path = "a")
public static @NotNull WrappedClientInformation createDefault()
{
return new WrappedClientInformation(invokeStaticWrappedMethod());
}
}
@@ -0,0 +1,34 @@
package de.eisi05.npc.api.wrapper.objects;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
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_5), path = "net.minecraft.commands.CommandListenerWrapper")
public class WrappedCommandSourceStack extends Wrapper
{
public WrappedCommandSourceStack(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "getBukkitSender")
public CommandSender getBukkitSender()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
public @NotNull WrappedCommandSourceStack withMaximumPermission()
{
return new WrappedCommandSourceStack(invokeWrappedMethod(4));
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
public @NotNull WrappedCommandSourceStack withSuppressedOutput()
{
return new WrappedCommandSourceStack(invokeWrappedMethod());
}
}
@@ -0,0 +1,333 @@
package de.eisi05.npc.api.wrapper.objects;
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 de.eisi05.npc.api.wrapper.enums.ChatFormat;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.function.UnaryOperator;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.chat.IChatBaseComponent")
public class WrappedComponent extends Wrapper
{
private WrappedComponent(Object handle)
{
super(handle);
}
public static WrappedComponent fromHandle(@Nullable Object handle)
{
return new WrappedComponent(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
public static @NotNull WrappedComponent create(@Nullable String text)
{
return new WrappedComponent(text == null ? CommonComponents.EMPTY : invokeStaticWrappedMethod(text));
}
public static @NotNull WrappedComponent parseFromLegacy(@Nullable String legacy)
{
if(legacy == null || legacy.isEmpty())
return create(null);
return Var.fromString(legacy);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
public @NotNull WrappedComponent setFormats(@NotNull ChatFormat... format)
{
if(format.length == 0)
return this;
try
{
Method[] methods = getHandle().getClass().getMethods();
String path = getPath();
Method target = Arrays.stream(methods)
.filter(m -> m.getName().equals(path))
.filter(Method::isVarArgs)
.filter(method -> method.getParameterTypes()[0].isArray())
.filter(method -> method.getParameterTypes()[0].componentType().equals(format[0].getHandle().getClass()))
.findFirst()
.orElseThrow();
target.invoke(getHandle(), (Object) Arrays.stream(format)
.map(ChatFormat::getHandle)
.toArray(i -> (Object[]) Array.newInstance(format[0].getHandle().getClass(), i)));
} catch(IllegalAccessException | InvocationTargetException | NoSuchElementException e)
{
}
return this;
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), path = "a")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "format")
public @NotNull WrappedComponent withClickEvent(@NotNull ClickEvent clickEvent)
{
invokeWrappedMethod((UnaryOperator<Object>) o -> new Style(o).withClickEvent(clickEvent).getHandle());
return this;
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), path = "a")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "format")
public @NotNull WrappedComponent withHoverEvent(@NotNull HoverEvent hoverEvent)
{
invokeWrappedMethod((UnaryOperator<Object>) o -> new Style(o).withHoverEvent(hoverEvent).getHandle());
return this;
}
@Mapping(range = @Mapping.Range(from = Versions.V1_19_1, to = Versions.V1_21_5), 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)
{
invokeWrappedMethod(wrappedComponent);
return this;
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_4, to = Versions.V1_21_5), 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()
{
return new WrappedComponent(invokeWrappedMethod());
}
public @NotNull String toLegacy()
{
return Var.toString(this);
}
public @NotNull String toLegacyNoColor()
{
return ChatColor.stripColor(toLegacy());
}
public @NotNull SerializedComponent serialize()
{
return new SerializedComponent(toLegacy());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_5), 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_5), 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);
private CommonComponents()
{
super(null);
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.chat.ChatClickable")
public static class ClickEvent extends Wrapper
{
private ClickEvent(Object handle)
{
super(handle);
}
public static @NotNull ClickEvent create(@NotNull ClickAction action, @NotNull Object value)
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5))
return createWrappedInstance(ClickEvent.class, action, value);
return action.function.apply(value);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5),
path = "net.minecraft.network.chat.ChatClickable$EnumClickAction")
public enum ClickAction implements EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
OPEN_URL(o -> new OpenUrl((URI) o)),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
OPEN_FILE(o -> new OpenFile((String) o)),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "c")
RUN_COMMAND(o -> new RunCommand((String) o)),
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "d")
SUGGEST_COMMAND(o -> new SuggestCommand((String) o)),
@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_17, to = Versions.V1_21_5), path = "f")
COPY_TO_CLIPBOARD(o -> new CopyToClipboard((String) o));
private final Function<Object, ClickEvent> function;
ClickAction(Function<Object, ClickEvent> function)
{
this.function = function;
}
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5),
path = "net.minecraft.network.chat.ChatClickable$OpenUrl")
private static class OpenUrl extends WrappedComponent.ClickEvent
{
private OpenUrl(URI url)
{
super(createInstance(OpenUrl.class, url));
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5),
path = "net.minecraft.network.chat.ChatClickable$OpenFile")
private static class OpenFile extends WrappedComponent.ClickEvent
{
private OpenFile(String file)
{
super(createInstance(OpenFile.class, file));
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5),
path = "net.minecraft.network.chat.ChatClickable$RunCommand")
private static class RunCommand extends WrappedComponent.ClickEvent
{
private RunCommand(String command)
{
super(createInstance(RunCommand.class, command));
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5),
path = "net.minecraft.network.chat.ChatClickable$SuggestCommand")
private static class SuggestCommand extends WrappedComponent.ClickEvent
{
private SuggestCommand(String command)
{
super(createInstance(SuggestCommand.class, command));
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5),
path = "net.minecraft.network.chat.ChatClickable$ChangePage")
private static class ChangePage extends WrappedComponent.ClickEvent
{
private ChangePage(int page)
{
super(createInstance(ChangePage.class, page));
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5),
path = "net.minecraft.network.chat.ChatClickable$CopyToClipboard")
private static class CopyToClipboard extends WrappedComponent.ClickEvent
{
private CopyToClipboard(String string)
{
super(createInstance(CopyToClipboard.class, string));
}
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.chat.ChatHoverable")
public static class HoverEvent extends Wrapper
{
private HoverEvent(Object handle)
{
super(handle);
}
public static HoverEvent create(@NotNull WrappedComponent component)
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5))
return createWrappedInstance(HoverEvent.class, HoverAction.SHOW_TEXT, component);
return new ShowText(component);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5), path = "net.minecraft.network.chat.ChatHoverable$e")
private static class ShowText extends HoverEvent
{
private ShowText(@NotNull WrappedComponent component)
{
super(createInstance(ShowText.class, component));
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5),
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_4), path = "a")
private static final Object SHOW_TEXT = getStaticWrappedFieldValue("SHOW_TEXT").orElse(null);
private HoverAction()
{
super(null);
}
}
}
public static class SerializedComponent implements Serializable
{
@Serial
private static final long serialVersionUID = 1L;
private final String text;
public SerializedComponent(@Nullable String text)
{
this.text = text;
}
public @NotNull WrappedComponent deserialize()
{
return WrappedComponent.parseFromLegacy(text);
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.chat.ChatModifier")
public static class Style extends Wrapper
{
private Style(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), 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_5), path = "a")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setChatHoverable")
public @NotNull Style withHoverEvent(@NotNull HoverEvent hoverEvent)
{
return new Style(invokeWrappedMethod(hoverEvent));
}
}
}
@@ -0,0 +1,91 @@
package de.eisi05.npc.api.wrapper.objects;
import com.mojang.authlib.GameProfile;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import de.eisi05.npc.api.wrapper.packets.PacketWrapper;
import io.netty.channel.Channel;
import org.jetbrains.annotations.NotNull;
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), path = "net.minecraft.server.network.PlayerConnection")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "net.minecraft.network.PlayerConnection")
public class WrappedConnection extends Wrapper
{
WrappedConnection(Object handle)
{
super(handle);
}
WrappedConnection(@NotNull WrappedMinecraftServer server, @NotNull WrappedNetworkManager networkManager, @NotNull WrappedServerPlayer player,
@NotNull CommonListenerCookie commonListenerCookie)
{
super(createInstance(WrappedConnection.class, server, networkManager, player, commonListenerCookie));
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_5), 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)
{
invokeWrappedMethod(packet);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), 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")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "a")
public @NotNull WrappedNetworkManager networkManager()
{
return new WrappedNetworkManager(getWrappedFieldValue());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.NetworkManager")
public static class WrappedNetworkManager extends Wrapper
{
private WrappedNetworkManager(Object handle)
{
super(handle);
}
public static @NotNull WrappedNetworkManager create(@NotNull PacketFlow packetFlow)
{
return createWrappedInstance(WrappedNetworkManager.class, packetFlow);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_5), 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")
public @NotNull Channel channel()
{
return getWrappedFieldValue();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_5),
path = "net.minecraft.network.protocol.EnumProtocolDirection")
public enum PacketFlow implements EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_5), path = "a")
SERVERBOUND,
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_5), path = "b")
CLIENTBOUND;
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_21_5), path = "net.minecraft.server.network.CommonListenerCookie")
public static class CommonListenerCookie extends Wrapper
{
public CommonListenerCookie(@NotNull GameProfile profile, int latency, boolean transferred)
{
super(createInstance(CommonListenerCookie.class, profile, latency, WrappedClientInformation.createDefault(), transferred));
}
}
}
@@ -0,0 +1,177 @@
package de.eisi05.npc.api.wrapper.objects;
import com.google.common.collect.ImmutableList;
import de.eisi05.npc.api.utils.Reflections;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import de.eisi05.npc.api.wrapper.packets.CustomPacket;
import de.eisi05.npc.api.wrapper.packets.PacketWrapper;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
public abstract class WrappedEntity<T extends Entity> extends Wrapper
{
protected WrappedEntity(Object handle)
{
super(handle);
}
public static <T extends Entity, V extends WrappedEntity<T>> @NotNull V fromEntity(@NotNull T entity, @NotNull Class<V> clazz)
{
try
{
Constructor<V> constructor = clazz.getDeclaredConstructor(Object.class);
constructor.setAccessible(true);
return constructor.newInstance(Reflections.invokeMethod(entity, "getHandle").get());
} catch(NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e)
{
return null;
}
}
public static <T extends WrappedEntity<? extends Entity>> boolean is(WrappedEntity<?> entity, Class<T> clazz)
{
Mapping[] mappings = clazz.getAnnotationsByType(Mapping.class);
for(Mapping mapping : mappings)
{
if(!Versions.containsCurrentVersion(mapping))
continue;
return getTargetClass(mapping).equals(entity.getHandle().getClass());
}
return false;
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "getBukkitEntity")
public @NotNull T getBukkitPlayer()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), path = "ar")
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "au")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21), path = "ar")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_6), path = "ap")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "an")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "al")
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20), path = "aj")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "al")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_19_1), path = "ai")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getDataWatcher")
public @NotNull WrappedEntityData getEntityData()
{
return new WrappedEntityData(invokeWrappedMethod());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5), path = "u")
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "q")
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21), path = "p")
@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)
{
setWrappedFieldValue(ImmutableList.copyOf(Arrays.stream(entities).map(wrappedEntity -> wrappedEntity.handle).toList()));
}
@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")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "cK")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "cJ")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20), path = "cH")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_4), path = "cG")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "cH")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_1), path = "cC")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19), path = "cD")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_18_2), path = "cA")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getWorld")
public @NotNull Object getServer()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_5), 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")
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20), path = "S")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "T")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_19_1), path = "S")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getPacket")
public @NotNull PacketWrapper getAddEntityPacket()
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21))
return new CustomPacket(invokeWrappedMethod());
return new CustomPacket(invokeWrappedMethod(new WrappedEntityTrackerEntry(this)));
}
@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")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "cH")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "cG")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20), path = "cE")
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_19_4), path = "cD")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "cz")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_18_2), path = "cw")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getBoundingBox")
public BoundingBox getBoundingBox()
{
return new BoundingBox(invokeWrappedMethod());
}
public int getId()
{
return getBukkitPlayer().getEntityId();
}
public @NotNull World getWorld()
{
return getBukkitPlayer().getWorld();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.world.entity.EntityTypes")
public static class EntityTypes extends Wrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5), 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);
private EntityTypes()
{
super(null);
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.world.phys.AxisAlignedBB")
public static class BoundingBox extends Wrapper
{
private BoundingBox(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "c")
public double getYSize()
{
return invokeWrappedMethod();
}
}
}
@@ -0,0 +1,279 @@
package de.eisi05.npc.api.wrapper.objects;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import de.eisi05.npc.api.wrapper.enums.Pose;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.UUID;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.syncher.DataWatcher")
public class WrappedEntityData extends Wrapper
{
WrappedEntityData(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), 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);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
public @NotNull List<?> packDirty()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), 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_5), path = "net.minecraft.network.syncher.DataWatcherObject")
public static class EntityDataAccessor<T> extends Wrapper
{
private EntityDataAccessor(Object handle)
{
super(handle);
}
public static <T> @NotNull EntityDataAccessor<T> create(int i, @NotNull Object serializer)
{
return createWrappedInstance(EntityDataAccessor.class, i, serializer);
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.syncher.DataWatcherSerializer")
public static class EntityDataSerializer<T> extends Wrapper
{
private EntityDataSerializer(Object handle)
{
super(handle);
}
public @NotNull EntityDataAccessor<T> create(int i)
{
return EntityDataAccessor.create(i, this.handle);
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.syncher.DataWatcherRegistry")
public static class EntityDataSerializers extends Wrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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_5), 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")
public static final EntityDataSerializer<?> ENUM_DIRECTION = new EntityDataSerializer<>(
getStaticWrappedFieldValue("ENUM_DIRECTION").orElse(null));
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_4), path = "r")
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "q")
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "p")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "o")
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_5), 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_5), 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")
public static final EntityDataSerializer<Optional<?>> OPTIONAL_GLOBAL_POS = new EntityDataSerializer<>(
getStaticWrappedFieldValue("OPTIONAL_GLOBAL_POS").orElse(null));
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), path = "t")
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_20_4), path = "s")
@Mapping(fixed = @Mapping.Fixed(value = Versions.V1_19_3), path = "r")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "q")
@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_20_6, to = Versions.V1_21_5), 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")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "r")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "q")
public static final EntityDataSerializer<?> VILLAGER_DATA = new EntityDataSerializer<>(
getStaticWrappedFieldValue("VILLAGER_DATA").orElse(null));
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), 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")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "s")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "r")
public static final EntityDataSerializer<OptionalInt> OPTIONAL_INT = new EntityDataSerializer<>(
getStaticWrappedFieldValue("OPTIONAL_INT").orElse(null));
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), 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")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "t")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "s")
public static final EntityDataSerializer<Pose> ENTITY_POSE = new EntityDataSerializer<>(
getStaticWrappedFieldValue("ENTITY_POSE").orElse(null));
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), 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<?> VECTOR3 = new EntityDataSerializer<>(
getStaticWrappedFieldValue("VECTOR3").orElse(null));
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5), 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));
private EntityDataSerializers()
{
super(null);
}
}
}
@@ -0,0 +1,35 @@
package de.eisi05.npc.api.wrapper.objects;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set;
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_5), path = "net.minecraft.server.level.EntityTrackerEntry")
public class WrappedEntityTrackerEntry extends Wrapper
{
public WrappedEntityTrackerEntry(@NotNull WrappedEntity<?> entity)
{
super(createHandle(entity));
}
private static @NotNull Object createHandle(@NotNull WrappedEntity<?> entity)
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5))
return createInstance(WrappedEntityTrackerEntry.class, entity.getServer(), entity, 0, false, (Consumer<Object>) o ->
{
}, Set.of());
else
return createInstance(WrappedEntityTrackerEntry.class, entity.getServer(), entity, 0, false, (Consumer<Object>) o ->
{
}, (BiConsumer<Object, List<UUID>>) (o, uuids) ->
{
}, Set.of());
}
}
@@ -0,0 +1,132 @@
package de.eisi05.npc.api.wrapper.objects;
import com.mojang.brigadier.CommandDispatcher;
import de.eisi05.npc.api.utils.Reflections;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import java.util.Properties;
import java.util.function.UnaryOperator;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.server.MinecraftServer")
public class WrappedMinecraftServer extends Wrapper
{
public static final WrappedMinecraftServer INSTANCE = new WrappedMinecraftServer(Reflections.invokeMethod(Bukkit.getServer(), "getServer").get());
private WrappedMinecraftServer(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "vanillaCommandDispatcher")
public @NotNull WrappedMinecraftServer commands()
{
return new WrappedMinecraftServer(getWrappedFieldValue());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), path = "a")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getDedicatedServerProperties")
public WrappedServerProperties getProperties()
{
return new WrappedServerProperties(invokeWrappedMethod());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "s")
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21), path = "r")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "s")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_20_2), path = "u")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_18_2), path = "y")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_18), path = "z")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "x")
public WrappedServerSettings settings()
{
return new WrappedServerSettings(getWrappedFieldValue());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "options")
public Object options()
{
return getWrappedFieldValue();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), 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")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "aU")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20), path = "aV")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_4), path = "aX")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "aW")
public Object registryAccess()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
@SuppressWarnings("unchecked")
public @NotNull CommandDispatcher<Object> getCommandDispatcher()
{
return (CommandDispatcher<Object>) Reflections.invokeMethod(commands().getHandle(), getPath()).get();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5),
path = "net.minecraft.server.dedicated.DedicatedServerProperties")
public static class WrappedServerProperties extends Wrapper
{
private WrappedServerProperties(Object handle)
{
super(handle);
}
public WrappedServerProperties(Properties properties, Object options)
{
super(createInstance(WrappedServerProperties.class, properties, options));
}
public static WrappedServerProperties fromHandle(Object handle)
{
return new WrappedServerProperties(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "X")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21), path = "Y")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_6), path = "X")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_20_4), path = "W")
public boolean enforceSecureProfile()
{
return getWrappedFieldValue();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_5), path = "ac")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_6), path = "ab")
@Mapping(range = @Mapping.Range(from = Versions.V1_20_2, to = Versions.V1_20_4), path = "Z")
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_20), path = "Y")
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_19_1), path = "X")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_18_2), path = "Y")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "X")
public Properties properties()
{
return getWrappedFieldValue();
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5),
path = "net.minecraft.server.dedicated.DedicatedServerSettings")
public static class WrappedServerSettings extends Wrapper
{
private WrappedServerSettings(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), path = "a")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setProperty")
public WrappedServerSettings update(UnaryOperator<Object> unaryOperator)
{
invokeWrappedMethod(unaryOperator);
return this;
}
}
}
@@ -0,0 +1,106 @@
package de.eisi05.npc.api.wrapper.objects;
import de.eisi05.npc.api.utils.Reflections;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
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 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_5), path = "net.minecraft.world.scores.ScoreboardTeam")
public class WrappedPlayerTeam extends Wrapper
{
private static final Map<UUID, Map<String, WrappedPlayerTeam>> teams = new HashMap<>();
private WrappedPlayerTeam(Object handle)
{
super(handle);
}
public static @NotNull WrappedPlayerTeam create(@NotNull Player player, @NotNull String name)
{
if(exists(player, name))
return teams.get(player.getUniqueId()).get(name);
Object scoreboard = Reflections.invokeMethod(player.getScoreboard(), "getHandle").get();
WrappedPlayerTeam wrappedPlayerTeam = createWrappedInstance(WrappedPlayerTeam.class, scoreboard, name);
var map = teams.getOrDefault(player.getUniqueId(), new HashMap<>());
map.put(name, wrappedPlayerTeam);
teams.put(player.getUniqueId(), map);
return wrappedPlayerTeam;
}
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_5), 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_5), 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()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5), 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()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), 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_5), 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_5), path = "net.minecraft.world.scores" +
".ScoreboardTeamBase$EnumNameTagVisibility")
public enum Visibility implements EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
ALWAYS,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
NEVER,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "c")
HIDE_FOR_OTHER_TEAMS,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "d")
HIDE_FOR_OWN_TEAM;
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
}
@@ -0,0 +1,222 @@
package de.eisi05.npc.api.wrapper.objects;
import com.mojang.authlib.GameProfile;
import de.eisi05.npc.api.utils.Reflections;
import de.eisi05.npc.api.utils.Var;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.utils.exceptions.VersionNotFound;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.enums.Pose;
import de.eisi05.npc.api.wrapper.packets.BundlePacket;
import de.eisi05.npc.api.wrapper.packets.ChatPacket;
import de.eisi05.npc.api.wrapper.packets.PacketWrapper;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.UUID;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.server.level.EntityPlayer")
public class WrappedServerPlayer extends WrappedEntity<Player>
{
private WrappedArmorStand armorStand;
WrappedServerPlayer(Object serverPlayer)
{
super(serverPlayer);
}
public static @NotNull WrappedServerPlayer fromPlayer(@NotNull Player player)
{
return fromEntity(player, WrappedServerPlayer.class);
}
public static @NotNull WrappedServerPlayer create(@NotNull Location location, @NotNull UUID uuid, @NotNull WrappedComponent name)
{
Object mcServer = WrappedMinecraftServer.INSTANCE.getHandle();
Object serverLevel = Var.getNmsLevel(location.getWorld());
GameProfile gameProfile = new GameProfile(uuid, "NPC" + uuid.toString().substring(0, 13));
Mapping[] annotations = WrappedServerPlayer.class.getAnnotationsByType(Mapping.class);
Class<?> targetClass = null;
for(Mapping mapping : annotations)
{
if(!Versions.containsCurrentVersion(mapping))
continue;
targetClass = getTargetClass(mapping);
break;
}
if(targetClass == null)
throw new VersionNotFound(targetClass);
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_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 ->
{
WrappedServerPlayer serverPlayer = createWrappedInstance(WrappedServerPlayer.class, mcServer, serverLevel, gameProfile,
WrappedClientInformation.createDefault().getHandle());
serverPlayer.setConnection(new WrappedConnection(WrappedMinecraftServer.INSTANCE,
WrappedConnection.WrappedNetworkManager.create(WrappedConnection.WrappedNetworkManager.PacketFlow.CLIENTBOUND),
serverPlayer, new WrappedConnection.CommonListenerCookie(gameProfile, 0, true)));
yield serverPlayer;
}
case NONE -> throw new VersionNotFound();
};
WrappedArmorStand armorStand = WrappedArmorStand.create(location.getWorld());
armorStand.moveTo(location.clone().add(0, 0.2, 0));
wrappedServerPlayer.setNameTag(armorStand);
wrappedServerPlayer.setListName(name);
return wrappedServerPlayer;
}
@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)
{
return new WrappedAttributeInstance(invokeWrappedMethod(attribute));
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_5), path = "gi")
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "gh")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21), path = "fX")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_6), path = "gb")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "fR")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "fQ")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20), path = "fM")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_4), path = "fI")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "fD")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_1), path = "fy")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19), path = "fz")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_18_2), path = "fq")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_18), path = "fp")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getProfile")
public @NotNull GameProfile getGameProfile()
{
return invokeWrappedMethod();
}
@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")
public @NotNull WrappedConnection playerConnection()
{
return new WrappedConnection(getWrappedFieldValue());
}
@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")
public void setConnection(@NotNull WrappedConnection newConnection)
{
setWrappedFieldValue(newConnection.getHandle());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "listName")
public void setListName(@Nullable WrappedComponent component)
{
setWrappedFieldValue(component.getHandle());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "listName")
public void setListName(@Nullable String name)
{
setListName(WrappedComponent.create(name));
}
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20), path = "f")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_4), path = "e")
public void setLatency(int i)
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_20_2))
setWrappedFieldValue(i);
else
setConnection(new WrappedConnection(WrappedMinecraftServer.INSTANCE, playerConnection().networkManager(), this,
new WrappedConnection.CommonListenerCookie(getGameProfile(), i, true)));
}
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_5), 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_5, to = Versions.V1_21_5), path = "z")
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_4), path = "A")
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21), path = "dg")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_4), path = "dd")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20_2), path = "dc")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_20), path = "da")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_4), path = "cZ")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_3), path = "cY")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19_1), path = "cT")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_19), path = "cU")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_18_2), path = "cQ")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getCommandListener")
public WrappedCommandSourceStack createCommandStack()
{
return new WrappedCommandSourceStack(invokeWrappedMethod());
}
@Mapping(range = @Mapping.Range(from = Versions.V1_19, to = Versions.V1_21_5), path = "a")
public void sendRawMessage(@NotNull WrappedComponent component)
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19))
sendPacket(new ChatPacket(component, ChatPacket.ChatType.CHAT));
else
invokeWrappedMethod(component);
}
public @NotNull String getName()
{
return getGameProfile().getName();
}
public void setName(@NotNull String name)
{
Reflections.setField(getGameProfile(), "name", name);
}
public @NotNull UUID getUUID()
{
return getGameProfile().getId();
}
public boolean isOnGround()
{
return ((Entity) getBukkitPlayer()).isOnGround();
}
public void sendPacket(@NotNull PacketWrapper packet)
{
if(packet instanceof BundlePacket bundlePacket)
Arrays.stream(bundlePacket.getPackets()).forEach(this::sendPacket);
else
playerConnection().sendPacket(packet);
}
public WrappedArmorStand getNameTag()
{
return armorStand;
}
public void setNameTag(WrappedArmorStand armorStand)
{
setPassengers(armorStand);
this.armorStand = armorStand;
}
}
@@ -0,0 +1,45 @@
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 org.jetbrains.annotations.NotNull;
import java.io.Serializable;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game.PacketPlayOutAnimation")
public class AnimatePacket extends PacketWrapper
{
protected AnimatePacket(@NotNull WrappedServerPlayer player, int animationId)
{
super(AnimatePacket.class, player, animationId);
}
public static PacketWrapper create(@NotNull WrappedServerPlayer player, @NotNull Animation animation)
{
if(animation != Animation.HURT || Versions.isCurrentVersionSmallerThan(Versions.V1_19_4))
return new AnimatePacket(player, animation.ordinal());
return new HurtAnimationPacket(player);
}
public enum Animation implements Serializable
{
SWING_MAIN_HAND,
HURT,
WAKE_UP,
SWING_OFF_HAND,
CRITICAL_HIT,
MAGIC_CRITICAL_HIT
}
@Mapping(range = @Mapping.Range(from = Versions.V1_19_4, to = Versions.V1_21_5),
path = "net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket")
public static class HurtAnimationPacket extends PacketWrapper
{
private HurtAnimationPacket(@NotNull WrappedServerPlayer player)
{
super(HurtAnimationPacket.class, player);
}
}
}
@@ -0,0 +1,19 @@
package de.eisi05.npc.api.wrapper.packets;
import org.jetbrains.annotations.NotNull;
public class BundlePacket extends PacketWrapper
{
private final PacketWrapper[] packets;
public BundlePacket(@NotNull PacketWrapper... packets)
{
super(null);
this.packets = packets;
}
public @NotNull PacketWrapper[] getPackets()
{
return packets;
}
}
@@ -0,0 +1,38 @@
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.WrappedComponent;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "net.minecraft.network.protocol.game.PacketPlayOutChat")
public class ChatPacket extends PacketWrapper
{
private static final UUID NULL_UUID = new UUID(0, 0);
public ChatPacket(@NotNull WrappedComponent component, @NotNull ChatType type)
{
super(ChatPacket.class, component, type, NULL_UUID);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "net.minecraft.network.chat.ChatMessageType")
public enum ChatType implements EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "a")
CHAT,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "b")
SYSTEM,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_18_2), path = "c")
GAME_INFO;
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
}
@@ -0,0 +1,9 @@
package de.eisi05.npc.api.wrapper.packets;
public class CustomPacket extends PacketWrapper.PacketHolder
{
public CustomPacket(Object packet)
{
super(packet);
}
}
@@ -0,0 +1,18 @@
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_5), path = "net.minecraft.network.protocol.game.PacketPlayOutEntity")
public class MoveEntityPacket
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayOutEntity$PacketPlayOutEntityLook")
public static class Rot extends PacketWrapper
{
public Rot(int entityId, byte yRot, byte xRot, boolean onGround)
{
super(Rot.class, entityId, yRot, xRot, onGround);
}
}
}
@@ -0,0 +1,53 @@
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.Wrapper;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
import java.util.Arrays;
public abstract class PacketWrapper extends Wrapper
{
protected PacketWrapper(Object handle)
{
super(handle);
}
public <T extends Wrapper> PacketWrapper(@NotNull Class<T> clazz, Object... args)
{
super(createInstance(clazz, args));
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.Packet")
public static abstract class PacketHolder extends PacketWrapper
{
public PacketHolder(Object handle)
{
super(handle);
}
public static boolean is(@NotNull Object packet, @NotNull Class<? extends PacketHolder> clazz)
{
return Arrays.stream(clazz.getAnnotationsByType(Mapping.class))
.filter(Versions::containsCurrentVersion)
.findFirst()
.filter(data -> getTargetClass(data) != null)
.map(data -> getTargetClass(data).isAssignableFrom(packet.getClass())).orElse(false);
}
public static <T extends PacketHolder> @NotNull T wrap(@NotNull Object packet, @NotNull Class<T> clazz)
{
try
{
Constructor<T> constructor = clazz.getDeclaredConstructor(Object.class);
constructor.setAccessible(true);
return constructor.newInstance(packet);
} catch(Exception e)
{
throw new RuntimeException(e);
}
}
}
}
@@ -0,0 +1,18 @@
package de.eisi05.npc.api.wrapper.packets;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
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_5), path = "net.minecraft.network.protocol.game" +
".ClientboundPlayerInfoRemovePacket")
public class PlayerInfoRemovePacket extends PacketWrapper
{
public PlayerInfoRemovePacket(@NotNull List<UUID> list)
{
super(PlayerInfoRemovePacket.class, list);
}
}
@@ -0,0 +1,60 @@
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 org.jetbrains.annotations.NotNull;
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), 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
{
public PlayerInfoUpdatePacket(@NotNull Action action, @NotNull WrappedServerPlayer serverPlayer)
{
super(PlayerInfoUpdatePacket.class, action, serverPlayer);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), 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_5), path = "a")
ADD_PLAYER,
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), path = "b")
INITIALIZE_CHAT,
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), 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_5), path = "d")
UPDATE_LISTED,
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), 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_5), 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.V1_17, to = Versions.V1_19_1), path = "e")
REMOVE_PLAYER,
@Mapping(range = @Mapping.Range(from = Versions.V1_21_2, to = Versions.V1_21_5), path = "g")
UPDATE_LIST_ORDER,
@Mapping(range = @Mapping.Range(from = Versions.V1_21_4, to = Versions.V1_21_5), path = "h")
UPDATE_HAT;
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
}
@@ -0,0 +1,14 @@
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_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayOutEntityDestroy")
public class RemoveEntityPacket extends PacketWrapper
{
public RemoveEntityPacket(int entityId)
{
super(RemoveEntityPacket.class, entityId);
}
}
@@ -0,0 +1,16 @@
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 org.jetbrains.annotations.NotNull;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayOutEntityHeadRotation")
public class RotateHeadPacket extends PacketWrapper
{
public RotateHeadPacket(@NotNull WrappedServerPlayer serverPlayer, byte yHeadRot)
{
super(RotateHeadPacket.class, serverPlayer, yHeadRot);
}
}
@@ -0,0 +1,37 @@
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.WrappedEntityData;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayOutEntityMetadata")
public class SetEntityDataPacket extends PacketWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_19_1), path = "")
private SetEntityDataPacket(int id, @NotNull WrappedEntityData data, boolean dirty)
{
super(SetEntityDataPacket.class, id, data, dirty);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_19_3, to = Versions.V1_21_5), path = "")
private SetEntityDataPacket(int id, @NotNull List<?> data)
{
super(SetEntityDataPacket.class, id, data);
}
public static SetEntityDataPacket create(int id, @NotNull WrappedEntityData data, boolean nonDefaults)
{
if(Versions.isCurrentVersionSmallerThan(Versions.V1_19_3))
return new SetEntityDataPacket(id, data, nonDefaults);
else
{
List<?> dataList = nonDefaults ? data.getAll() : data.packDirty();
return new SetEntityDataPacket(id, dataList == null ? new ArrayList<>() : dataList);
}
}
}
@@ -0,0 +1,18 @@
package de.eisi05.npc.api.wrapper.packets;
import com.mojang.datafixers.util.Pair;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayOutEntityEquipment")
public class SetEquipmentPacket extends PacketWrapper
{
public SetEquipmentPacket(int entity, @NotNull List<Pair<?, ?>> list)
{
super(SetEquipmentPacket.class, entity, list);
}
}
@@ -0,0 +1,14 @@
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;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game.PacketPlayOutMount")
public class SetPassengerPacket extends PacketWrapper
{
public SetPassengerPacket(WrappedEntity<?> entity)
{
super(SetPassengerPacket.class, entity.getHandle());
}
}
@@ -0,0 +1,53 @@
package de.eisi05.npc.api.wrapper.packets;
import com.google.common.collect.ImmutableList;
import de.eisi05.npc.api.utils.Versions;
import de.eisi05.npc.api.wrapper.Mapping;
import de.eisi05.npc.api.wrapper.Wrapper;
import de.eisi05.npc.api.wrapper.objects.WrappedPlayerTeam;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Optional;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayOutScoreboardTeam")
public class SetPlayerTeamPacket extends PacketWrapper
{
private SetPlayerTeamPacket(@NotNull String teamName, int method, @NotNull Optional<Object> parameter, @NotNull Collection<String> players)
{
super(SetPlayerTeamPacket.class, teamName, method, parameter, players);
}
public static SetPlayerTeamPacket createAddOrModifyPacket(@NotNull WrappedPlayerTeam team, boolean create)
{
return new SetPlayerTeamPacket(team.getName(), create ? 0 : 2, Optional.of(new Parameters(team).getHandle()),
(create ? team.getPlayers() : ImmutableList.of()));
}
public static SetPlayerTeamPacket createRemovePacket(@NotNull WrappedPlayerTeam team)
{
return new SetPlayerTeamPacket(team.getName(), 1, Optional.empty(), ImmutableList.of());
}
public static SetPlayerTeamPacket createPlayerPacket(@NotNull WrappedPlayerTeam team, @NotNull String playerName, @NotNull Action action)
{
return new SetPlayerTeamPacket(team.getName(), action == Action.ADD ? 3 : 4, Optional.empty(), ImmutableList.of(playerName));
}
public enum Action
{
ADD,
REMOVE
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayOutScoreboardTeam$b")
public static class Parameters extends Wrapper
{
public Parameters(@NotNull WrappedPlayerTeam team)
{
super(createInstance(Parameters.class, team.getHandle()));
}
}
}
@@ -0,0 +1,17 @@
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.WrappedAttributeInstance;
import java.util.List;
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_5),
path = "net.minecraft.network.protocol.game.PacketPlayOutUpdateAttributes")
public class UpdateAttributesPacket extends PacketWrapper
{
public UpdateAttributesPacket(int id, WrappedAttributeInstance instance)
{
super(UpdateAttributesPacket.class, id, List.of(instance.getHandle()));
}
}
@@ -0,0 +1,103 @@
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.Wrapper;
import de.eisi05.npc.api.wrapper.enums.InteractionHand;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game.PacketPlayInUseEntity")
public class UseEntityPacketWrapper extends PacketWrapper.PacketHolder
{
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_5), path = "e")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_20_6), path = "d")
public final static Action ATTACK_ACTION = new Action(getStaticWrappedFieldValue("ATTACK_ACTION").orElse(null));
protected UseEntityPacketWrapper(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_5), path = "b")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_20_6), path = "a")
public int getId()
{
return getWrappedFieldValue();
}
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_5), path = "c")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_20_6), path = "b")
public @NotNull Action getAction()
{
Object value = getWrappedFieldValue();
if(value.equals(ATTACK_ACTION.getHandle()))
return ATTACK_ACTION;
return new Action(value);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayInUseEntity$b")
public enum ActionType implements EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
INTERACT,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "b")
ATTACK,
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "c")
INTERACT_AT;
@Override
public @NotNull Object getHandle()
{
return cast(this);
}
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "net.minecraft.network.protocol.game" +
".PacketPlayInUseEntity$EnumEntityUseAction")
public static class Action extends Wrapper
{
public Action(Object handle)
{
super(handle);
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
public @Nullable ActionType getActionType()
{
Object enumHandle = invokeWrappedMethod();
for(ActionType type : ActionType.values())
{
if(type.getHandle().equals(enumHandle))
return type;
}
return null;
}
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_5), path = "a")
public @Nullable InteractionHand getHand()
{
try
{
Object enumHandle = getWrappedFieldValue();
for(InteractionHand type : InteractionHand.values())
{
if(type.getHandle().equals(enumHandle))
return type;
}
return null;
} catch(RuntimeException e)
{
if(e.getCause() instanceof RuntimeException ex && ex.getCause() instanceof NoSuchFieldException)
return null;
else
throw e;
}
}
}
}