Compare commits

...

74 Commits

Author SHA1 Message Date
Gabriel Tofvesson
7c9bf21ced Implement sign editing 2021-07-24 21:38:06 +02:00
Gabriel Tofvesson
f4cb5f58e5 Integrate new Quality-of-Life commands 2021-07-24 00:04:02 +02:00
Gabriel Tofvesson
3fde89e1ca Implement new Quality-of-Life commands 2021-07-23 23:55:34 +02:00
Gabriel Tofvesson
5701a20cc8 Fix re-render boundary checks 2021-07-07 20:55:34 +02:00
Gabriel Tofvesson
97e17cba3f Update list of capitator targets 2021-07-06 21:57:30 +02:00
Gabriel Tofvesson
8acef93d7d Implement minimum food level option for capitator functionality 2021-07-03 03:22:14 +02:00
Gabriel Tofvesson
353f97a5e8 Implement configurable hunger penalty for capitator axe 2021-07-03 03:08:36 +02:00
Gabriel Tofvesson
a3f0fcf5aa Implement /search result ordering by distance 2021-06-30 00:04:05 +02:00
Gabriel Tofvesson
cb10fa6720 Move deployment script to gist: https://gist.github.com/GabrielTofvesson/eb093c1434f5541bd9984e7baac4d65f 2021-06-29 21:04:45 +02:00
Gabriel Tofvesson
1c2d966791 Fix configuration typo 2021-06-29 16:01:22 +02:00
Gabriel Tofvesson
092aa33303 Update version number 2021-06-27 21:58:20 +02:00
Gabriel Tofvesson
a740b34e4e Implement ItemStack finder command 2021-06-27 21:57:55 +02:00
Gabriel Tofvesson
b5d1079a55 Update version number 2021-06-27 19:30:45 +02:00
Gabriel Tofvesson
7b1c03ff69 Fix CI pipeline deprecation errors 2021-06-27 18:44:37 +02:00
Gabriel Tofvesson
fcc025a1a2 Fix CI pipeline classpath 2021-06-27 18:33:22 +02:00
Gabriel Tofvesson
9620b230e2 Include SpigotWizCompat dependency in CI pipeline 2021-06-27 18:30:36 +02:00
Gabriel Tofvesson
67b2488976 Implement name-tags for chests 2021-06-27 18:30:36 +02:00
Gabriel Tofvesson
6ac5c1de0e Fix assertion conditions 2021-06-27 18:30:36 +02:00
Gabriel Tofvesson
eb5b2beb41 Loft implementations to shared library plugin WizCompat 2021-06-27 18:30:54 +02:00
Gabriel Tofvesson
4082e51aa2 Implement preliminary chest name-tags 2021-06-27 18:30:36 +02:00
Gabriel Tofvesson
d912715741 Implement compatibility layer for fake entities 2021-06-27 18:30:36 +02:00
Gabriel Tofvesson
3b25897490 Update test server deploy script 2021-06-27 18:30:36 +02:00
Gabriel Tofvesson
b058f5f658 Prevent CI trigger on pull-request 2021-06-22 15:50:01 +02:00
Gabriel Tofvesson
7feda4b517 Add 1 extra use to capitator tools 2021-06-22 15:37:08 +02:00
Gabriel Tofvesson
6295fa609c Fix bug in GH workflow 2021-06-22 14:28:32 +02:00
Gabriel Tofvesson
a1cd3a476b Update CI artifact naming convention 2021-06-22 14:27:26 +02:00
Gabriel Tofvesson
6bcecb61ef Update version number 2021-06-20 18:42:15 +02:00
Gabriel Tofvesson
3cf7ffd1a2 Update code style config 2021-06-20 18:41:05 +02:00
Gabriel Tofvesson
382b2370ea Remove direct references to NMS in ghost-click implementation 2021-06-20 18:40:56 +02:00
Gabriel Tofvesson
3a0adad1f0 Implement configuration toggles for extra features 2021-06-20 18:22:54 +02:00
Gabriel Tofvesson
f7d9b7f30d Implement ghost-click 2021-06-20 18:22:54 +02:00
Gabriel Tofvesson
a22b4c715d Update target spigot API version for CI 2021-06-20 18:22:54 +02:00
Gabriel Tofvesson
7eb271c764 Update Java version 2021-06-20 18:22:54 +02:00
Gabriel Tofvesson
80704ba595 Update test server deployment script 2021-06-20 18:22:54 +02:00
Gabriel Tofvesson
0d37534eed Implement tree capitator 2021-06-20 18:22:54 +02:00
Gabriel Tofvesson
4a602b42f6 Update server deployment script 2021-05-20 20:00:23 +02:00
Gabriel Tofvesson
454c83c7f0 Add automatic Paper test server deployment script 2021-05-20 19:50:21 +02:00
Gabriel Tofvesson
8b2947e312 Fix stack clearing bug for throwable projectiles 2021-05-20 01:25:01 +02:00
Gabriel Tofvesson
c94f56f0ac Add stack count check for throwable replacement 2021-05-20 00:52:26 +02:00
Gabriel Tofvesson
f0c89e897b Fix JDK version differences 2021-05-20 00:48:37 +02:00
Gabriel Tofvesson
0a124dc16a Downgrade CI JDK to 15 for compatibility 2021-05-20 00:45:41 +02:00
Gabriel Tofvesson
318fe7f052 Merge remote-tracking branch 'origin/master' 2021-05-20 00:45:14 +02:00
Gabriel Tofvesson
9405176e09 Downgrade JDK version to 15 for compatibility 2021-05-20 00:44:55 +02:00
Gabriel Tofvesson
5e2e3c16bb
Update CI JDK version 2021-05-20 00:40:45 +02:00
Gabriel Tofvesson
711bcb1c70 Merge remote-tracking branch 'origin/master' 2021-05-20 00:37:22 +02:00
Gabriel Tofvesson
d22aa9f189 Implement stack replacement for throwables 2021-05-20 00:36:58 +02:00
Gabriel Tofvesson
96d519f84a Use updated jdk features 2021-05-20 00:36:58 +02:00
Gabriel Tofvesson
4eb5892517 Clean up tab completion implementation 2021-05-20 00:36:58 +02:00
Gabriel Tofvesson
5540b4332d Update JDK version 2021-05-20 00:36:58 +02:00
Gabriel Tofvesson
4dc4835758
Update ci.yml 2021-05-08 20:25:59 +02:00
Gabriel Tofvesson
0270890c03
Update ci.yml 2021-05-08 20:11:35 +02:00
Gabriel Tofvesson
cb5a0a07ca
Create ci.yml 2021-05-08 20:10:16 +02:00
Gabriel Tofvesson
5acc20f485 Update version number 2021-05-06 18:44:18 +02:00
Gabriel Tofvesson
257662d198 Improve fuzzy matching 2021-05-06 18:43:58 +02:00
Gabriel Tofvesson
424bc4f706 Fix fuzzy matching bug 2021-05-06 18:41:42 +02:00
Gabriel Tofvesson
5c380cccc3 Update version number 2021-05-06 17:35:31 +02:00
Gabriel Tofvesson
261932393d Add documentation 2021-05-06 17:35:13 +02:00
Gabriel Tofvesson
6c85283344 Fix command execution error output 2021-05-05 00:47:13 +02:00
Gabriel Tofvesson
791698adf9 Implement fuzzy matching for target material strings 2021-05-05 00:46:30 +02:00
Gabriel Tofvesson
ec2f4f1e27 Update JavaDoc command list 2021-05-04 06:31:46 +02:00
Gabriel Tofvesson
abd49226db Update version number 2021-05-04 06:25:41 +02:00
Gabriel Tofvesson
bd3fa2d4f8 Add tab completion listener to plugin 2021-05-04 06:25:04 +02:00
Gabriel Tofvesson
258f44879a Convert search command argument to namespaced key 2021-05-04 06:24:45 +02:00
Gabriel Tofvesson
39947d460f Implement tab completion for commands 2021-05-04 06:24:12 +02:00
Gabriel Tofvesson
ca0d62b85f Enable /search for plugin 2021-05-04 05:56:02 +02:00
Gabriel Tofvesson
9add7134e5 Implement /search 2021-05-04 05:55:34 +02:00
Gabriel Tofvesson
69e827dc08 Loft configuration behaviour for magnet command 2021-05-04 05:55:18 +02:00
Gabriel Tofvesson
3ab058adfb Loft configuration behaviour for command executors 2021-05-04 05:54:51 +02:00
Gabriel Tofvesson
0e8ab30672 Implement search configuration type 2021-05-04 05:54:18 +02:00
Gabriel Tofvesson
76aa22ec27 Add /search config settings 2021-05-04 05:49:26 +02:00
Gabriel Tofvesson
f3a061a942 Open double-chest directly 2021-05-04 04:21:07 +02:00
Gabriel Tofvesson
5e8a0ebc54 Implement basic inventory search command 2021-05-04 04:17:34 +02:00
Gabriel Tofvesson
17bd4950d8 Add command-related utility functions 2021-05-04 04:17:34 +02:00
Gabriel Tofvesson
1ce8ed4d82 Improve Java version compatibility 2021-05-04 04:17:34 +02:00
34 changed files with 2358 additions and 343 deletions

59
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,59 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Setup Java JDK
uses: actions/setup-java@v2.0.0
with:
# The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file
java-version: '16'
# Java distribution. See the list of supported distributions in README file
distribution: 'adopt'
# Runs a single command using the runners shell
- name: Compile
run: |
mkdir -p build
jar_url=$(curl https://hub.spigotmc.org/nexus/content/repositories/snapshots/org/spigotmc/spigot-api/1.17-R0.1-SNAPSHOT/ | grep "shaded.jar\"" | tail -1 | sed "s/.*href=\"\(.*\)\".*/\1/g")
curl $jar_url > spigot-shaded.jar
find -name "*.java" > sources
curl -s https://github.com/GabrielTofvesson/SpigotWizCompat/releases | grep "latest/SpigotWizCompat-.*jar" | cut -d : -f 2,3 | sed "s/[^\"]*\"\\([^\"]*\\)\".*/https:\\/\\/github.com\\/\\1/g" | wget -O SpigotWizCompat.jar -qi -
javac -Xlint:deprecation -classpath "spigot-shaded.jar:SpigotWizCompat.jar" @sources -d build
cp res/* build
rm sources
cd build
jar cvf build.jar *
- name: Rename Artifacts
run: |
cd build
mv build.jar $(sha1sum build.jar | sed "s/\([^ ]*\) .*/SpigotInvTweaks-\\1.jar/g")
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "CI Build"
files: |
build/SpigotInvTweaks-*.jar

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

9
.idea/libraries/SpigotWizCompat.xml generated Normal file
View File

@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="SpigotWizCompat">
<CLASSES>
<root url="jar://$PROJECT_DIR$/lib/SpigotWizCompat.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

2
.idea/misc.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_15" project-jdk-name="15" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="16" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -6,8 +6,10 @@
<sourceFolder url="file://$MODULE_DIR$/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="15" jdkType="JavaSDK" />
<orderEntry type="jdk" jdkName="16" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="spigot-1.15.5-R0.1" level="project" />
<orderEntry type="library" name="SpigotWizCompat" level="project" />
<orderEntry type="library" name="SpigotWizCompat" level="project" />
</component>
</module>

View File

@ -2,4 +2,13 @@ magnet:
==: dev.w1zzrd.invtweaks.serialization.MagnetConfig
radius: 8.0
interval: 5
subdivide: 1
subdivide: 1
search:
==: dev.w1zzrd.invtweaks.serialization.SearchConfig
searchRadiusX: 8
searchRadiusY: 8
searchRadiusZ: 8
ghostClick: true
capitator: true
capitatorHungerPerBlock: 0.03125
capitatorMinHunger: 3

View File

@ -1,8 +1,10 @@
name: InventoryTweaks
version: 1.2.0
version: 1.4.1
author: IKEA_Jesus
main: dev.w1zzrd.invtweaks.InvTweaksPlugin
api-version: 1.13
depend:
- WizCompat
commands:
sort:
description: Sort chest you are looking at
@ -11,4 +13,28 @@ commands:
magnet:
description: Toggle item magnet mode for player
usage: /<command>
permission: invtweaks.magnet
permission: invtweaks.magnet
search:
description: Search for a given item in all nearby inventories
usage: /<command> {item type}
permission: invtweaks.search
find:
description: Show all chests that contain a given item
usage: /<command> {item type}
permission: invtweaks.find
capitator:
description: Toggle tree capitation for an axe in your main hand
usage: /<command>
permission: invtweaks.capitator
chestname:
description: Add a floating nametag to a chest
usage: /<command> {name}
permission: invtweaks.spawnfake
growup:
description: Age baby animals
usage: /<command> [radius]
permission: invtweaks.growup
rewool:
description: Put the wool back on sheep
usage: /<command> [radius]
permission: invtweaks.rewool

View File

@ -1,93 +0,0 @@
package dev.w1zzrd.invtweaks;
import org.bukkit.Bukkit;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Manager for persistent data storage for a plugin
*/
public class DataStore {
private static final Logger logger = Bukkit.getLogger();
private final File storeFile;
private final FileConfiguration config;
/**
* Create a data store with the given name. This will attempt to load a yaml file named after the store
* @param storeName Name of the store to load. File will be named storeName + ".yml"
* @param plugin Plugin to associate the data store with
*/
public DataStore(final String storeName, final Plugin plugin) {
storeFile = new File(plugin.getDataFolder(), storeName + ".yml");
config = YamlConfiguration.loadConfiguration(storeFile);
// Save config in case it doesn't exist
saveData();
}
/**
* Load a value from the data store
* @param path Path in the file to load the data from
* @param defaultValue Getter for a default value, in case the data does not exist in the store
* @param <T> Type of the data to load
* @return Data at the given path, if available, else the default value
*/
public <T extends ConfigurationSerializable> T loadData(final String path, final DefaultGetter<T> defaultValue) {
final T value = (T) config.get(path);
return value == null ? defaultValue.get() : value;
}
/**
* Save data at a given path in the store
* @param path Path to store data at
* @param value Data to store
* @param <T> Type of {@link ConfigurationSerializable} data to store
*/
public <T extends ConfigurationSerializable> void storeData(final String path, final T value) {
config.set(path, value);
}
/**
* Save the current data store in memory to persistent memory
*/
public void saveData() {
try {
config.save(storeFile);
} catch (IOException e) {
logger.log(Level.SEVERE, Logger.GLOBAL_LOGGER_NAME + " Could not save data due to an I/O error", e);
}
}
/**
* Reload data store from persistent memory, overwriting any current state
*/
public void loadData() {
try {
config.load(storeFile);
} catch (IOException | InvalidConfigurationException e) {
logger.log(Level.SEVERE, Logger.GLOBAL_LOGGER_NAME + " Could not load data due to an I/O error", e);
}
}
/**
* Functional interface for constructing default values
* @param <T> Type to construct
*/
public interface DefaultGetter<T> {
/**
* Instantiate default value
* @return Default value that was instantiated
*/
T get();
}
}

View File

@ -1,14 +1,18 @@
package dev.w1zzrd.invtweaks;
import dev.w1zzrd.invtweaks.command.MagnetCommandExecutor;
import dev.w1zzrd.invtweaks.command.SortCommandExecutor;
import dev.w1zzrd.invtweaks.listener.MagnetismListener;
import dev.w1zzrd.invtweaks.command.*;
import dev.w1zzrd.invtweaks.enchantment.CapitatorEnchantment;
import dev.w1zzrd.invtweaks.feature.NamedChestManager;
import dev.w1zzrd.invtweaks.listener.*;
import dev.w1zzrd.invtweaks.serialization.ChestNameConfig;
import dev.w1zzrd.invtweaks.serialization.MagnetConfig;
import dev.w1zzrd.invtweaks.listener.SortListener;
import dev.w1zzrd.invtweaks.listener.StackReplaceListener;
import dev.w1zzrd.invtweaks.serialization.MagnetData;
import dev.w1zzrd.invtweaks.serialization.UUIDList;
import dev.w1zzrd.invtweaks.serialization.SearchConfig;
import dev.w1zzrd.spigot.wizcompat.enchantment.EnchantmentRegistryEntry;
import dev.w1zzrd.spigot.wizcompat.enchantment.ServerEnchantmentRegistry;
import dev.w1zzrd.spigot.wizcompat.serialization.PersistentData;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.PluginManager;
@ -28,18 +32,29 @@ public final class InvTweaksPlugin extends JavaPlugin {
public static final String LOG_PLUGIN_NAME = "[InventoryTweaks]";
private static final String PERSISTENT_DATA_NAME = "data";
private static final String ENCHANTMENT_CAPITATOR_NAME = "Capitator";
private final Logger logger = Bukkit.getLogger();
// Command executor references in case I need them or something idk
private SortCommandExecutor sortCommandExecutor;
private MagnetCommandExecutor magnetCommandExecutor;
private DataStore data;
private SearchCommandExecutor searchCommandExecutor;
private NamedChestCommand namedChestCommandExecutor;
private FindCommandExecutor findCommandExecutor;
private CapitatorCommand capitatorCommand;
private GrowUpCommand growUpCommand;
private ReWoolCommand reWoolCommand;
private PersistentData data;
private NamedChestManager chestManager;
private EnchantmentRegistryEntry<CapitatorEnchantment> capitatorEnchantment = null;
@Override
public void onEnable() {
logger.fine(LOG_PLUGIN_NAME + " Plugin enabled");
enablePersistentData();
initEnchantments();
initCommands();
initEvents();
}
@ -50,6 +65,7 @@ public final class InvTweaksPlugin extends JavaPlugin {
disableEvents();
disableCommands();
disableEnchantments();
disablePersistentData();
}
@ -61,47 +77,60 @@ public final class InvTweaksPlugin extends JavaPlugin {
if (magnetCommandExecutor != null)
magnetCommandExecutor.reloadConfig();
if (searchCommandExecutor != null)
searchCommandExecutor.reloadConfig();
}
/**
* Get a reference to the persistent data store object for this plugin
* @return An instance of {@link DataStore} for this plugin
* @return An instance of {@link PersistentData} for this plugin
*/
public DataStore getPersistentData() {
public PersistentData getPersistentData() {
return data;
}
/**
* Initialize commands registered by this plugin
*/
private void initCommands() {
sortCommandExecutor = new SortCommandExecutor();
magnetCommandExecutor = new MagnetCommandExecutor(this, getPersistentData());
private void initEnchantments() {
if (getConfig().getBoolean("capitator", true))
capitatorEnchantment = ServerEnchantmentRegistry.registerEnchantment(
this,
new CapitatorEnchantment(
ENCHANTMENT_CAPITATOR_NAME,
new NamespacedKey(this, ENCHANTMENT_CAPITATOR_NAME)
)
);
}
// TODO: Bind command by annotation
Objects.requireNonNull(getCommand("sort")).setExecutor(sortCommandExecutor);
Objects.requireNonNull(getCommand("magnet")).setExecutor(magnetCommandExecutor);
private void disableEnchantments() {
if (getConfig().getBoolean("capitator", true))
ServerEnchantmentRegistry.unRegisterEnchantment(this, capitatorEnchantment);
capitatorEnchantment = null;
}
/**
* Initialize event listeners for this plugin
*/
private void initEvents() {
final boolean activateCapitator = getConfig().getBoolean("capitator", true);
final boolean activateGhostClick = getConfig().getBoolean("ghostClick", true);
final PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new StackReplaceListener(), this);
if (activateGhostClick)
pluginManager.registerEvents(new GhostClickListener(), this);
pluginManager.registerEvents(new StackReplaceListener(this), this);
pluginManager.registerEvents(new SortListener(), this);
pluginManager.registerEvents(new MagnetismListener(magnetCommandExecutor), this);
}
/**
* Do whatever is necessary to disable commands and their execution
*/
private void disableCommands() {
magnetCommandExecutor.onDisable();
sortCommandExecutor = null;
magnetCommandExecutor = null;
pluginManager.registerEvents(new TabCompletionListener(), this);
pluginManager.registerEvents(new TreeCapitatorListener(
activateCapitator ? capitatorEnchantment.getEnchantment() : null,
getConfig().getDouble("capitatorHungerPerBlock"),
getConfig().getInt("capitatorMinHunger")
), this);
pluginManager.registerEvents(new PlayerMoveRenderListener(chestManager), this);
pluginManager.registerEvents(new ChestBreakListener(chestManager), this);
pluginManager.registerEvents(new SignEditListener(), this);
}
/**
@ -112,6 +141,50 @@ public final class InvTweaksPlugin extends JavaPlugin {
HandlerList.unregisterAll(this);
}
/**
* Initialize commands registered by this plugin
*/
private void initCommands() {
final boolean activateCapitator = getConfig().getBoolean("capitator", true);
sortCommandExecutor = new SortCommandExecutor();
magnetCommandExecutor = new MagnetCommandExecutor(this, "magnet", getPersistentData());
searchCommandExecutor = new SearchCommandExecutor(this, "search");
namedChestCommandExecutor = new NamedChestCommand(chestManager);
findCommandExecutor = new FindCommandExecutor(this);
growUpCommand = new GrowUpCommand();
reWoolCommand = new ReWoolCommand();
if (activateCapitator)
capitatorCommand = new CapitatorCommand(capitatorEnchantment.getEnchantment());
// TODO: Bind command by annotation
Objects.requireNonNull(getCommand("sort")).setExecutor(sortCommandExecutor);
Objects.requireNonNull(getCommand("magnet")).setExecutor(magnetCommandExecutor);
Objects.requireNonNull(getCommand("search")).setExecutor(searchCommandExecutor);
Objects.requireNonNull(getCommand("chestname")).setExecutor(namedChestCommandExecutor);
Objects.requireNonNull(getCommand("find")).setExecutor(findCommandExecutor);
Objects.requireNonNull(getCommand("growup")).setExecutor(growUpCommand);
Objects.requireNonNull(getCommand("rewool")).setExecutor(reWoolCommand);
if (activateCapitator)
Objects.requireNonNull(getCommand("capitator")).setExecutor(capitatorCommand);
}
/**
* Do whatever is necessary to disable commands and their execution
*/
private void disableCommands() {
magnetCommandExecutor.onDisable();
capitatorCommand = null;
findCommandExecutor = null;
namedChestCommandExecutor = null;
searchCommandExecutor = null;
magnetCommandExecutor = null;
sortCommandExecutor = null;
}
/**
* Register type serializers/deserializers for configurations and YAML files
*
@ -120,7 +193,11 @@ public final class InvTweaksPlugin extends JavaPlugin {
private void registerSerializers() {
ConfigurationSerialization.registerClass(MagnetConfig.class);
ConfigurationSerialization.registerClass(MagnetData.class);
ConfigurationSerialization.registerClass(UUIDList.class);
ConfigurationSerialization.registerClass(SearchConfig.class);
ConfigurationSerialization.registerClass(ChestNameConfig.class);
ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.class);
ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.class);
ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry.class);
}
/**
@ -129,9 +206,13 @@ public final class InvTweaksPlugin extends JavaPlugin {
* @see #registerSerializers()
*/
private void unregisterSerializers() {
ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry.class);
ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.class);
ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.class);
ConfigurationSerialization.registerClass(ChestNameConfig.class);
ConfigurationSerialization.unregisterClass(MagnetConfig.class);
ConfigurationSerialization.unregisterClass(MagnetData.class);
ConfigurationSerialization.unregisterClass(UUIDList.class);
ConfigurationSerialization.unregisterClass(SearchConfig.class);
}
/**
@ -145,17 +226,21 @@ public final class InvTweaksPlugin extends JavaPlugin {
saveConfig();
// Implicit load
data = new DataStore(PERSISTENT_DATA_NAME, this);
data = new PersistentData(PERSISTENT_DATA_NAME, this);
chestManager = new NamedChestManager(data);
}
/**
* De-activate and finalize persistent data storage sources and handlers
*/
private void disablePersistentData() {
chestManager = null;
data.saveData();
data = null;
saveConfig();
//saveConfig();
unregisterSerializers();
}

View File

@ -0,0 +1,43 @@
package dev.w1zzrd.invtweaks.command;
import dev.w1zzrd.spigot.wizcompat.command.CommandUtils;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
public class CapitatorCommand implements CommandExecutor {
private final Enchantment capitatorEnchantment;
public CapitatorCommand(final Enchantment capitatorEnchantment) {
this.capitatorEnchantment = capitatorEnchantment;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (capitatorEnchantment == null) {
sender.spigot().sendMessage(CommandUtils.errorMessage("Tree capitation is disabled!"));
return true;
}
if (sender instanceof final Player caller) {
final ItemStack stack = caller.getInventory().getItemInMainHand();
if (stack.getType().name().endsWith("_AXE")) {
if (stack.containsEnchantment(capitatorEnchantment)) {
stack.removeEnchantment(capitatorEnchantment);
sender.spigot().sendMessage(CommandUtils.successMessage("Item is now a regular axe"));
} else {
stack.addEnchantment(capitatorEnchantment, 1);
sender.spigot().sendMessage(CommandUtils.successMessage("Item is now a capitator axe"));
}
} else
sender.spigot().sendMessage(CommandUtils.errorMessage("Only axes can be tree capitators!"));
} else
sender.spigot().sendMessage(CommandUtils.errorMessage("Only players can create tree capitators!"));
return true;
}
}

View File

@ -0,0 +1,43 @@
package dev.w1zzrd.invtweaks.command;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import java.util.stream.Stream;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.errorMessage;
public final class Commands {
private Commands() { throw new UnsupportedOperationException("Functional class"); }
public static double getCommandRadius(final CommandSender sender, final String[] args, final int radiusIndex) {
try {
if (args.length > radiusIndex)
return Double.parseDouble(args[radiusIndex]);
} catch (NumberFormatException e) {
sender.spigot().sendMessage(errorMessage(String.format("\"%s\" is not a number", args[radiusIndex])));
}
return Double.NaN;
}
public static <T extends Entity> Stream<T> getCommandEntityRadius(
final CommandSender sender,
final String[] args,
final int radiusIndex,
final Class<T> entityType
) {
final double radius = Commands.getCommandRadius(sender, args, 0);
if (Double.compare(radius, Double.NaN) == 0)
return null;
return
(radius >= 0 ?
((Player) sender).getNearbyEntities(radius, radius, radius).stream() :
Bukkit.getWorlds().stream().flatMap(it -> it.getEntities().stream()))
.filter(it -> entityType.isAssignableFrom(it.getClass()))
.map(it -> (T) it);
}
}

View File

@ -0,0 +1,98 @@
package dev.w1zzrd.invtweaks.command;
import dev.w1zzrd.spigot.wizcompat.command.CommandUtils;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Container;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static dev.w1zzrd.invtweaks.listener.TabCompletionListener.getMaterialMatching;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue;
import static dev.w1zzrd.spigot.wizcompat.packet.EntityCreator.*;
public final class FindCommandExecutor implements CommandExecutor {
private static final int SEARCH_RADIUS = 3;
private static final long DESPAWN_WAIT = 20 * 10;
private final Plugin plugin;
public FindCommandExecutor(final Plugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
Material targetMaterial;
if (assertTrue(sender instanceof Player, "Command can only be run by players", sender) ||
assertTrue(args.length == 1, "Exactly one argument is expected", sender) ||
assertTrue((targetMaterial = getMaterialMatching(args[0])) != null, String.format("Unknown item/block: %s", args[0]), sender))
return true;
final Player player = (Player) sender;
final List<BlockState> matches = searchChunks(
player.getLocation().getChunk(),
SEARCH_RADIUS,
Material.CHEST, Material.TRAPPED_CHEST, Material.SHULKER_BOX
);
final List<Integer> entities = matches.stream()
.filter(it -> Arrays.stream(((Container)it).getSnapshotInventory().getContents()).filter(Objects::nonNull).map(ItemStack::getType).anyMatch(targetMaterial::equals))
.map(state -> spawnMarker(player, state.getLocation().add(0.5, 0.2, 0.5)))
.collect(Collectors.toList());
if (assertTrue(entities.size() > 0, "No containers neardby contain that item/block", sender))
return true;
Bukkit.getScheduler().runTaskLater(plugin, () -> {
entities.forEach(it -> sendEntityDespawnPacket(player, it));
}, DESPAWN_WAIT);
return true;
}
private static int spawnMarker(final Player target, final Location location) {
final Object entity = createFakeSlime(target);
setSlimeSize(entity, 1);
setEntityCollision(entity, false);
setEntityInvisible(entity, true);
setEntityInvulnerable(entity, true);
setEntityGlowing(entity, true);
setEntityLocation(entity, location.getX(), location.getY(), location.getZ(), 0f, 0f);
sendEntitySpawnPacket(target, entity);
sendEntityMetadataPacket(target, entity);
return getEntityID(entity);
}
private static List<BlockState> searchChunks(final Chunk middle, final int radius, final Material... targets) {
final int xMin = middle.getX() - radius;
final int xMax = middle.getX() + radius;
final int zMin = middle.getZ() - radius;
final int zMax = middle.getZ() + radius;
final World sourceWorld = middle.getWorld();
final List<Material> targetMaterials = Arrays.asList(targets);
final ArrayList<BlockState> collect = new ArrayList<>();
for (int x = xMin; x <= xMax; ++x)
for (int z = zMin; z <= zMax; ++z)
Arrays.stream(sourceWorld.getChunkAt(x, z).getTileEntities()).filter(it -> targetMaterials.contains(it.getType())).forEach(collect::add);
return collect;
}
}

View File

@ -0,0 +1,28 @@
package dev.w1zzrd.invtweaks.command;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Ageable;
import org.bukkit.entity.Breedable;
import org.bukkit.entity.Player;
import java.util.stream.Stream;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue;
public class GrowUpCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (assertTrue(args.length == 0 || sender instanceof Player, "Only players can run this command", sender))
return true;
final Stream<Breedable> breedables = Commands.getCommandEntityRadius(sender, args, 0, Breedable.class);
if (breedables == null)
return true;
breedables.filter(it -> !it.getAgeLock() && !it.isAdult()).forEach(Ageable::setAdult);
return true;
}
}

View File

@ -1,13 +1,13 @@
package dev.w1zzrd.invtweaks.command;
import dev.w1zzrd.invtweaks.DataStore;
import dev.w1zzrd.invtweaks.serialization.MagnetConfig;
import dev.w1zzrd.invtweaks.serialization.MagnetData;
import dev.w1zzrd.spigot.wizcompat.command.ConfigurableCommandExecutor;
import dev.w1zzrd.spigot.wizcompat.serialization.PersistentData;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
@ -24,18 +24,13 @@ import static dev.w1zzrd.invtweaks.InvTweaksPlugin.LOG_PLUGIN_NAME;
/**
* Handler for executions of /magnet command
*/
public class MagnetCommandExecutor implements CommandExecutor {
public class MagnetCommandExecutor extends ConfigurableCommandExecutor<MagnetConfig> {
private static final Logger logger = Bukkit.getLogger();
private static final String CONFIG_PATH = "magnet";
private final Plugin plugin;
private MagnetConfig config;
private final DataStore data;
private final PersistentData data;
private final MagnetData magnetData;
private int divIndex = 0;
private BukkitTask refreshTask = null;
@ -44,26 +39,16 @@ public class MagnetCommandExecutor implements CommandExecutor {
* Initialize the magnet executor and manger
* @param plugin Owner plugin for this executor
*/
public MagnetCommandExecutor(final Plugin plugin, final DataStore data) {
this.plugin = plugin;
public MagnetCommandExecutor(final Plugin plugin, final String path, final PersistentData data) {
super(plugin, path);
this.data = data;
this.magnetData = data.loadData("magnets", MagnetData::blank);
// Don't call reloadConfig to ensure we don't leak `this` during construction (a bit pedantic)
config = loadConfig(plugin);
}
/**
* Reload magnet command configuration
*/
public void reloadConfig() {
config = loadConfig(plugin);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player))
return false;
return true;
final boolean isMagnetActive = toggleMagnet((Player) sender);
@ -189,6 +174,9 @@ public class MagnetCommandExecutor implements CommandExecutor {
* disabled.
*/
public void updateMagnetismTask(final boolean checkOnline) {
final MagnetConfig config = getConfig();
final Plugin plugin = getPlugin();
if (refreshTask == null && (!checkOnline || magnetData.onlineMagnets() > 0) && plugin.isEnabled()) {
refreshTask = Bukkit.getScheduler().runTaskTimer(plugin, this::taskApplyMagnetism, 0, config.getInterval());
logger.info(LOG_PLUGIN_NAME + " Activated magnetism check task");
@ -210,6 +198,8 @@ public class MagnetCommandExecutor implements CommandExecutor {
final List<UUID> activeMagnets = magnetData.getOnlineMagnetsView();
final int size = activeMagnets.size();
final MagnetConfig config = getConfig();
final int subdivide = config.getSubdivide();
final double sqRadius = config.getRadius();
@ -264,16 +254,4 @@ public class MagnetCommandExecutor implements CommandExecutor {
public void onDisable() {
data.storeData("magnets", magnetData);
}
/**
* Load magnet configuration data for given plugin
* @param plugin Plugin for which to load configuration for
* @return Configuration from persistent data if available, else default configuration values
*/
private static MagnetConfig loadConfig(final Plugin plugin) {
return (MagnetConfig) plugin.getConfig().get(
CONFIG_PATH,
MagnetConfig.getDefault(plugin, CONFIG_PATH)
);
}
}

View File

@ -0,0 +1,52 @@
package dev.w1zzrd.invtweaks.command;
import dev.w1zzrd.invtweaks.feature.NamedChestManager;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import static dev.w1zzrd.invtweaks.listener.PlayerMoveRenderListener.RENDER_RADIUS;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue;
public final class NamedChestCommand implements CommandExecutor {
private final NamedChestManager manager;
public NamedChestCommand(final NamedChestManager manager) {
this.manager = manager;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (assertTrue(sender instanceof Player && ((Player) sender).isOnline(), "Command can only be run by a player!", sender))
return true;
if (assertTrue(args.length <= 1, "Too many arguments for command", sender))
return true;
final Player player = (Player) sender;
final Block block = player.getTargetBlockExact(10);
if (assertTrue(block != null && (block.getType() == Material.CHEST || block.getType() == Material.TRAPPED_CHEST), "You must be targeting a chest", sender))
return true;
final Chest chest = (Chest) block.getState();
if (args.length == 0) {
manager.removeTag(chest);
} else {
if (manager.hasNamedChest(chest))
manager.removeTag(chest);
manager.addTag(chest, args[0]);
}
manager.renderTags(chest.getChunk(), RENDER_RADIUS);
return true;
}
}

View File

@ -0,0 +1,27 @@
package dev.w1zzrd.invtweaks.command;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.entity.Sheep;
import java.util.stream.Stream;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue;
public class ReWoolCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (assertTrue(args.length == 0 || sender instanceof Player, "Only players can run this command", sender))
return true;
final Stream<Sheep> sheep = Commands.getCommandEntityRadius(sender, args, 0, Sheep.class);
if (sheep == null)
return true;
sheep.forEach(it -> it.setSheared(false));
return true;
}
}

View File

@ -0,0 +1,195 @@
package dev.w1zzrd.invtweaks.command;
import dev.w1zzrd.invtweaks.InvTweaksPlugin;
import dev.w1zzrd.invtweaks.serialization.SearchConfig;
import dev.w1zzrd.spigot.wizcompat.command.ConfigurableCommandExecutor;
import org.bukkit.*;
import org.bukkit.block.*;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.Comparator;
import java.util.logging.Logger;
import static dev.w1zzrd.invtweaks.listener.TabCompletionListener.getMaterialMatching;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue;
/**
* Handler for executions of /search command
*/
public class SearchCommandExecutor extends ConfigurableCommandExecutor<SearchConfig> {
private static final Logger logger = Bukkit.getLogger();
// TODO: Move to config (magic values)
private static final String
ERR_NOT_PLAYER = "Command must be run by an in-game player",
ERR_NO_ARG = "Command expects an item or block name as an argument",
ERR_UNKNOWN = "Unknown item/block name \"%s\"",
ERR_NO_INVENTORIES = "No inventories could be found";
public SearchCommandExecutor(final Plugin plugin, final String path) {
super(plugin, path);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
final Material targetMaterial;
if (assertTrue(sender instanceof Player, ERR_NOT_PLAYER, sender) ||
assertTrue(args.length == 1, ERR_NO_ARG, sender) ||
assertTrue((targetMaterial = getMaterialMatching(args[0])) != null, String.format(ERR_UNKNOWN, args[0]), sender)
) return true;
final Player player = (Player) sender;
final Location playerLocation = player.getLocation();
final SearchConfig config = getConfig();
final List<BlockState> matches = searchChunks(
playerLocation.getChunk(),
config.getSearchRadiusX(),
Material.CHEST, Material.TRAPPED_CHEST, Material.SHULKER_BOX
);
// Ensure we found inventory-holding blocks
if (assertTrue(matches.size() != 0, ERR_NO_INVENTORIES, sender))
return true;
matches.sort(Comparator.comparingDouble(state -> state.getLocation().distanceSquared(playerLocation)));
final InventoryHolder result;
FIND_RESULT:
{
for (final BlockState check : matches) {
final InventoryHolder holder;
if (check instanceof Chest)
holder = Objects.requireNonNull(((Chest) check).getBlockInventory().getHolder()).getInventory().getHolder();
else if (check instanceof ShulkerBox)
holder = ((ShulkerBox) check).getInventory().getHolder();
else {
logger.info(InvTweaksPlugin.LOG_PLUGIN_NAME + " Found non-matching block");
continue;
}
assert holder != null;
if (holder instanceof DoubleChest) {
final InventoryHolder left = Objects.requireNonNull(((DoubleChest) holder).getLeftSide());
final InventoryHolder right = Objects.requireNonNull(((DoubleChest) holder).getRightSide());
if (left.getInventory().contains(targetMaterial) || right.getInventory().contains(targetMaterial)) {
result = holder;
break FIND_RESULT;
}
} else if (holder.getInventory().contains(targetMaterial)) {
result = holder;
break FIND_RESULT;
}
}
assertTrue(false, "Could not find inventory with target item/block", sender);
return true;
}
if (result instanceof DoubleChest) {
final DoubleChest dChest = (DoubleChest) result;
// Black magic to make chest lid animation behave for double chests
try {
final Field tileField = dChest.getInventory().getClass().getDeclaredField("tile");
tileField.setAccessible(true);
final Object tile = tileField.get(dChest.getInventory());
final Field tecField = tile.getClass().getDeclaredField("tileentitychest");
tecField.setAccessible(true);
final Field entityField = player.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("entity");
entityField.setAccessible(true);
final Object entity = entityField.get(player);
final Method openContainerMethod = entity.getClass().getDeclaredMethod("openContainer", tile.getClass().getInterfaces()[0]);
openContainerMethod.setAccessible(true);
openContainerMethod.invoke(entity, tile);
final Field activeContainerField = entity.getClass().getSuperclass().getDeclaredField("activeContainer");
activeContainerField.setAccessible(true);
final Object activeContainer = activeContainerField.get(entity);
final Field checkReachableField = activeContainer.getClass().getSuperclass().getDeclaredField("checkReachable");
checkReachableField.setAccessible(true);
// Disable reach checks for container while it is open
checkReachableField.set(activeContainer, false);
return true;
} catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
logger.fine(InvTweaksPlugin.LOG_PLUGIN_NAME + " Could not use internal openContainer method; chest lids may stay open");
}
}
// Default behaviour for non-double-chest inventories
// (plus fallback for double-chests on "unsupported" versions)
player.openInventory(result.getInventory());
return true;
}
private static List<BlockState> searchChunks(final Chunk middle, final int radius, final Material... targets) {
final int xMin = middle.getX() - radius;
final int xMax = middle.getX() + radius;
final int zMin = middle.getZ() - radius;
final int zMax = middle.getZ() + radius;
final World sourceWorld = middle.getWorld();
final List<Material> targetMaterials = Arrays.asList(targets);
final ArrayList<BlockState> collect = new ArrayList<>();
for (int x = xMin; x <= xMax; ++x)
for (int z = zMin; z <= zMax; ++z)
Arrays.stream(sourceWorld.getChunkAt(x, z).getTileEntities()).filter(it -> targetMaterials.contains(it.getType())).forEach(collect::add);
return collect;
}
private static List<BlockState> searchBlocks(
final Location centre,
final World world,
final int rx,
final int ry,
final int rz,
final Material... targets
) {
if (targets.length == 0)
return Collections.emptyList();
final int x = centre.getBlockX(), y = centre.getBlockY(), z = centre.getBlockZ();
final ArrayList<BlockState> matches = new ArrayList<>();
for (int dx = -rx; dx < rx; ++dx)
for (int dy = -ry; dy < ry; ++dy)
CHECK_Z:
for (int dz = -rz; dz < rz; ++dz) {
final Block check = world.getBlockAt(x + dx, y + dy, z + dz);
final Material checkMaterial = check.getType();
for (Material target : targets)
if (target == checkMaterial) {
matches.add(check.getState());
continue CHECK_Z;
}
}
return matches;
}
}

View File

@ -19,6 +19,7 @@ import java.util.Optional;
import java.util.logging.Logger;
import static dev.w1zzrd.invtweaks.InvTweaksPlugin.LOG_PLUGIN_NAME;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue;
import static org.bukkit.Material.*;
/**
@ -32,12 +33,10 @@ public class SortCommandExecutor implements CommandExecutor {
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
// Since we rely on targeting an inventory either by what we're looking at or by whom it was called,
// there is an implicit dependency on that the command is called by (at the very least) an entity
if (!(sender instanceof Player)) {
logger.info(LOG_PLUGIN_NAME + " Sort command triggered by non-player");
sender.sendMessage("Command must be run by a player");
return false;
}
if (assertTrue(sender instanceof Player, "Command must be run by a player", sender))
return true;
assert sender instanceof Player;
final Player player = (Player) sender;
logger.fine(LOG_PLUGIN_NAME + " Sort triggered by player " + player.getName());

View File

@ -0,0 +1,55 @@
package dev.w1zzrd.invtweaks.enchantment;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.enchantments.EnchantmentTarget;
import org.bukkit.inventory.ItemStack;
public final class CapitatorEnchantment extends Enchantment {
private final String name;
public CapitatorEnchantment(String name, NamespacedKey key) {
super(key);
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public int getMaxLevel() {
return 1;
}
@Override
public int getStartLevel() {
return 1;
}
@Override
public EnchantmentTarget getItemTarget() {
return EnchantmentTarget.TOOL;
}
@Override
public boolean isTreasure() {
return false;
}
@Override
public boolean isCursed() {
return false;
}
@Override
public boolean conflictsWith(Enchantment other) {
return false;
}
@Override
public boolean canEnchantItem(ItemStack item) {
return item.getType().name().endsWith("_AXE");
}
}

View File

@ -0,0 +1,471 @@
package dev.w1zzrd.invtweaks.feature;
import dev.w1zzrd.invtweaks.serialization.ChestNameConfig;
import dev.w1zzrd.spigot.wizcompat.packet.EntityCreator;
import dev.w1zzrd.spigot.wizcompat.serialization.PersistentData;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.stream.Stream;
import static dev.w1zzrd.spigot.wizcompat.block.Chests.*;
import static dev.w1zzrd.spigot.wizcompat.packet.EntityCreator.*;
public final class NamedChestManager {
private static final String PATH_NAMED_CHESTS = "namedChests";
private final RenderRegistry renders = new RenderRegistry();
private final ChestNameConfig config;
public NamedChestManager(final PersistentData data) {
config = data.loadData(PATH_NAMED_CHESTS, ChestNameConfig::new);
}
public boolean hasNamedChest(final World world, final Location loc) {
return getChestNameAt(world, loc) != null;
}
public boolean hasNamedChest(final Location loc) {
return hasNamedChest(Objects.requireNonNull(loc.getWorld()), loc);
}
public boolean hasNamedChest(final Chest chest) {
final Block left = getLeftChest(chest);
return hasNamedChest(left.getWorld(), left.getLocation());
}
private String getChestName(final Block block) {
return getChestNameAt(block.getWorld(), getLeftChest((Chest)block.getState()).getLocation());
}
private void setChestName(final Block block, final String name) {
addChestName(block.getWorld(), block.getLocation(), name);
}
private void addChestName(final World world, final Location location, final String name) {
config.getEntry(world.getUID()).add(location, name);
}
public String getChestNameAt(final World world, final Location location) {
return config.getEntry(world.getUID()).getName(location);
}
public String getChestNameAt(final Location location) {
return getChestNameAt(Objects.requireNonNull(location.getWorld()), location);
}
public void untrackPlayer(final Player player) {
renders.removeRender(player.getUniqueId());
}
public void renderTags(final Chunk chunk, final int chunkRadius) {
chunk.getWorld()
.getPlayers()
.stream()
.filter(it -> {
final Chunk playerChunk = it.getLocation().getChunk();
return Math.abs(playerChunk.getX() - chunk.getX()) <= chunkRadius && Math.abs(playerChunk.getZ() - chunk.getZ()) <= chunkRadius;
})
.forEach(player -> renderTags(player, chunkRadius));
final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chestChunk = config.getChunkEntry(
chunk.getWorld().getUID(),
chunk.getX(),
chunk.getZ()
);
if (chestChunk != null)
chestChunk.setDirty(false);
}
public void renderTags(final Player target, final int chunkRadius) {
final UUID worldID = target.getWorld().getUID();
renders.updateRenders(
target,
chunkRadius,
addedChunk -> {
final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chunk = config.getChunkEntry(worldID, addedChunk.getRender().x(), addedChunk.getRender().z());
if (chunk == null)
return;
final int baseX = chunk.getX() << 4;
final int baseZ = chunk.getZ() << 4;
chunk.streamEntries().forEach(entry -> {
final Object entity = entry.getEntity(() -> {
final Location loc = getCenterChestLocation(target.getWorld().getBlockAt(baseX + entry.getChunkX(), entry.getY(), baseZ + entry.getChunkZ()));
final Object newEntity = createFakeSlime(target);
setEntityCollision(newEntity, false);
setEntityInvulnerable(newEntity, true);
setEntityLocation(newEntity, loc.getX(), loc.getY(), loc.getZ(), 0f, 0f);
setEntityCustomName(newEntity, entry.getName());
setEntityCustomNameVisible(newEntity, true);
return newEntity;
});
sendEntitySpawnPacket(target, entity);
sendEntityMetadataPacket(target, entity);
});
},
removedChunk -> {
final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chunk = config.getChunkEntry(worldID, removedChunk.getRender().x(), removedChunk.getRender().z());
if (chunk == null)
return;
sendEntityDespawnPackets(
target,
chunk.streamEntries()
.map(entry -> entry.getEntity(() -> null))
.filter(Objects::nonNull)
.mapToInt(EntityCreator::getEntityID).toArray()
);
});
}
public void removeTag(final World world, final Location location) {
final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry entry = config.getEntryAt(world.getUID(), location);
if (entry != null) {
final ChunkRenderEntry chunk = renders.getChunk(location.getChunk().getX(), location.getChunk().getZ(), false);
if (chunk != null) {
final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chestChunk = config.getChunkEntry(world.getUID(), chunk.getRender().x(), chunk.getRender().z());
if (chestChunk != null)
chestChunk.removeEntry(entry);
chunk.streamRenders().forEach(it -> {
final Player player = Bukkit.getPlayer(it.getRender());
if (player == null)
renders.removeRender(it.getRender());
else
sendEntityDespawnPacket(player, getEntityID(entry.getEntity(() -> null)));
});
if (chestChunk != null) {
chestChunk.setDirty(false);
// Make sure we don't leave blank data in persistent data file
final ChestNameConfig.ChestNameWorldEntry worldEntry = config.getEntry(world.getUID());
worldEntry.deleteEmptyChunk(chestChunk);
config.deleteEmptyWorld(worldEntry);
}
}
}
}
public void removeTag(final Location location) {
removeTag(Objects.requireNonNull(location.getWorld()), location);
}
public void addTag(final World world, final Location location, final String name) {
config.getEntry(world.getUID(), true).add(location, name);
}
public void addTag(final Chest chest, final String name) {
final Block left = getLeftChest(chest);
addTag(left.getWorld(), left.getLocation(), name);
}
public void removeTag(final Chest chest) {
final Block left = getLeftChest(chest);
removeTag(left.getWorld(), left.getLocation());
}
private static Location getCenterChestLocation(final Block chestBlock) {
if (isDoubleChest(chestBlock)) {
final Location left = getBlockCenter(getLeftChest((Chest) chestBlock.getState()));
final Location right = getBlockCenter(getRightChest((Chest) chestBlock.getState()));
return new Location(left.getWorld(), (left.getX() + right.getX()) / 2.0, left.getY() + 0.2, (left.getZ() + right.getZ()) / 2.0);
} else {
return getBlockCenter(chestBlock).add(0.0, 0.2, 0.0);
}
}
private static Location getBlockCenter(final Block block) {
return block.getLocation().add(0.5, 0, 0.5);
}
private final class RenderRegistry {
private final List<PlayerRenderEntry> playerRegistry = new ArrayList<>();
private final List<ChunkRenderEntry> chunkRegistry = new ArrayList<>();
public void addRender(final Player target, final int chunkX, final int chunkZ) {
final PlayerRenderEntry player = getPlayer(target.getUniqueId(), true);
final ChunkRenderEntry chunk = getChunk(chunkX, chunkZ, true);
assert player != null;
player.addRender(chunk);
assert chunk != null;
chunk.addRender(player);
}
public void removeRender(final Player target) {
removeRender(target.getUniqueId());
}
void removeRender(final UUID offlinePlayer) {
final PlayerRenderEntry player = getPlayer(offlinePlayer, false);
if (player != null) {
player.streamRenders().forEach(this::doRemoveChunk);
doRemovePlayer(player);
}
}
public void removeRender(final int chunkX, final int chunkZ) {
final ChunkRenderEntry chunk = getChunk(chunkX, chunkZ, false);
if (chunk != null) {
chunk.streamRenders().forEach(this::doRemovePlayer);
doRemoveChunk(chunk);
}
}
private void doRemoveChunk(final ChunkRenderEntry chunk) {
final int index = Collections.binarySearch(chunkRegistry, chunk);
if (index >= 0)
chunkRegistry.remove(index);
}
private void doRemovePlayer(final PlayerRenderEntry player) {
final int index = Collections.binarySearch(playerRegistry, player);
if (index >= 0)
playerRegistry.remove(index);
}
public void updateRenders(final Player target, final int chunkRadius, final ChunkEntryChangeHandler onAdd, final ChunkEntryChangeHandler onRemove) {
final PlayerRenderEntry player = getPlayer(target.getUniqueId(), true);
final UUID worldID = target.getWorld().getUID();
final int chunkX = target.getLocation().getBlockX() >> 4;
final int chunkZ = target.getLocation().getBlockZ() >> 4;
final int xMax = chunkX + chunkRadius;
final int xMin = chunkX - chunkRadius;
final int zMax = chunkZ + chunkRadius;
final int zMin = chunkZ - chunkRadius;
assert player != null;
final List<ChunkRenderEntry> toRemove = new ArrayList<>();
player.streamRenders()
.filter(chunk -> {
final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chestChunk = config.getChunkEntry(worldID, chunk.getRender().x(), chunk.getRender().z());
return chunk.getRender().x() > xMax ||
chunk.getRender().x() < xMin ||
chunk.getRender().z() > zMax ||
chunk.getRender().z() < zMin ||
(chestChunk != null && chestChunk.isDirty());
}
)
.forEach(chunk -> {
toRemove.add(chunk);
onRemove.onChange(chunk);
});
toRemove.forEach(player::removeRender);
for (int x = xMin; x <= xMax; ++x)
for (int z = zMin; z <= zMax; ++z) {
final ChunkRenderEntry chunk = getChunk(x, z, true);
assert chunk != null;
if (player.addRender(chunk))
onAdd.onChange(chunk);
chunk.addRender(player);
}
}
public Stream<PlayerRenderEntry> streamPlayers() {
return playerRegistry.stream();
}
public Stream<ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry> streamEntries(final Player target) {
final PlayerRenderEntry player = getPlayer(target.getUniqueId(), false);
final UUID worldID = target.getWorld().getUID();
if (player == null)
return null;
else
return player.streamRenders()
.map(chunkEntry -> config.getChunkEntry(worldID, chunkEntry.getRender().x(), chunkEntry.getRender().z()));
}
private ChunkRenderEntry getChunk(final int chunkX, final int chunkZ, final boolean addIfMissing) {
final ChunkRenderEntry find = new ChunkRenderEntry(chunkX, chunkZ);
final int index = Collections.binarySearch(chunkRegistry, find);
if (index >= 0)
return chunkRegistry.get(index);
else if (addIfMissing) {
chunkRegistry.add(-(index + 1), find);
return find;
}
return null;
}
private PlayerRenderEntry getPlayer(final UUID player, final boolean addIfMissing) {
final PlayerRenderEntry find = new PlayerRenderEntry(player);
final int index = Collections.binarySearch(playerRegistry, find);
if (index >= 0)
return playerRegistry.get(index);
else if (addIfMissing) {
playerRegistry.add(-(index + 1), find);
return find;
}
return null;
}
}
private interface RenderEntry<T extends Comparable<T>, R> extends Comparable<RenderEntry<T, R>> {
T getRender();
boolean addRender(final R r);
boolean removeRender(final R r);
boolean containsRender(final R r);
Stream<R> streamRenders();
@Override
default int compareTo(final RenderEntry<T, R> o) {
return getRender().compareTo(o.getRender());
}
}
private static final class PlayerRenderEntry implements RenderEntry<UUID, ChunkRenderEntry> {
private final UUID player;
private final List<ChunkRenderEntry> chunks = new ArrayList<>();
public PlayerRenderEntry(final UUID player) {
this.player = player;
}
@Override
public UUID getRender() {
return player;
}
@Override
public boolean addRender(final ChunkRenderEntry chunk) {
final int index = Collections.binarySearch(chunks, chunk);
if (index < 0) {
chunks.add(-(index + 1), chunk);
return true;
}
return false;
}
@Override
public boolean removeRender(final ChunkRenderEntry chunk) {
final int index = Collections.binarySearch(chunks, chunk);
if (index >= 0) {
chunks.remove(index);
return true;
}
return false;
}
@Override
public boolean containsRender(final ChunkRenderEntry chunkRenderEntry) {
return Collections.binarySearch(chunks, chunkRenderEntry) >= 0;
}
@Override
public Stream<ChunkRenderEntry> streamRenders() {
return chunks.stream();
}
}
private static final class ChunkRenderEntry implements RenderEntry<ChunkRenderEntry.ChunkCoordinate, PlayerRenderEntry> {
private final ChunkCoordinate coords;
private final List<PlayerRenderEntry> players = new ArrayList<>();
public ChunkRenderEntry(final int chunkX, final int chunkZ) {
coords = new ChunkCoordinate(chunkX, chunkZ);
}
@Override
public ChunkCoordinate getRender() {
return coords;
}
@Override
public boolean addRender(final PlayerRenderEntry player) {
final int index = Collections.binarySearch(players, player);
if (index < 0) {
players.add(-(index + 1), player);
return true;
}
return false;
}
@Override
public boolean removeRender(final PlayerRenderEntry player) {
final int index = Collections.binarySearch(players, player);
if (index >= 0) {
players.remove(index);
return true;
}
return false;
}
@Override
public boolean containsRender(final PlayerRenderEntry chunkRenderEntry) {
return Collections.binarySearch(players, chunkRenderEntry) >= 0;
}
@Override
public Stream<PlayerRenderEntry> streamRenders() {
return players.stream();
}
public record ChunkCoordinate(int x, int z) implements Comparable<ChunkCoordinate> {
@Override
public int compareTo(final ChunkCoordinate o) {
final int compX = Integer.compare(x, o.x);
if (compX == 0)
return Integer.compare(z, o.z);
return compX;
}
}
}
private interface ChunkEntryChangeHandler {
void onChange(final ChunkRenderEntry chunk);
}
}

View File

@ -0,0 +1,43 @@
package dev.w1zzrd.invtweaks.listener;
import dev.w1zzrd.invtweaks.feature.NamedChestManager;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import static dev.w1zzrd.invtweaks.listener.PlayerMoveRenderListener.RENDER_RADIUS;
import static dev.w1zzrd.spigot.wizcompat.block.Chests.getLeftChest;
public final class ChestBreakListener implements Listener {
private final NamedChestManager manager;
public ChestBreakListener(final NamedChestManager manager) {
this.manager = manager;
}
@EventHandler
public void onChestBreak(final BlockBreakEvent event) {
if (!event.isCancelled())
processBlockEvent(event);
}
@EventHandler
public void onChestPlace(final BlockPlaceEvent event) {
if (!event.isCancelled())
processBlockEvent(event);
}
private void processBlockEvent(final BlockEvent event) {
if (event.getBlock().getType() == Material.CHEST || event.getBlock().getType() == Material.TRAPPED_CHEST) {
final Chest chest = (Chest) event.getBlock().getState();
manager.removeTag(chest);
manager.renderTags(chest.getChunk(), RENDER_RADIUS);
}
}
}

View File

@ -0,0 +1,133 @@
package dev.w1zzrd.invtweaks.listener;
import dev.w1zzrd.invtweaks.InvTweaksPlugin;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class GhostClickListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onEntityClick(final PlayerInteractEntityEvent clickEvent) {
if (clickEvent.getPlayer().getInventory().getItemInMainHand().getType() == Material.AIR &&
clickEvent.getPlayer().getInventory().getItemInOffHand().getType() == Material.AIR &&
clickEvent.getHand() == EquipmentSlot.HAND) {
final Block b = clickEvent.getPlayer().getTargetBlockExact(6);
if (b != null) {
try {
final Method getNMS = findDeclaredMethod(b.getClass(), "getNMS");
if (getNMS == null) {
Bukkit.getLogger().warning(InvTweaksPlugin.LOG_PLUGIN_NAME + " Cannot find NMS getter for blocks: ghost click will not function!");
return;
}
getNMS.setAccessible(true);
final Object nmsBlock = getNMS.invoke(b);
final Method interact = findDeclaredMethod(nmsBlock.getClass(), "interact");
if (interact == null) {
Bukkit.getLogger().warning(InvTweaksPlugin.LOG_PLUGIN_NAME + " Cannot find interact method for blocks: ghost click will not function!");
return;
}
interact.setAccessible(true);
final Field world = findDeclaredField(b.getClass(), "world");
if (world == null) {
Bukkit.getLogger().warning(InvTweaksPlugin.LOG_PLUGIN_NAME + " Cannot find world field for blocks: ghost click will not function!");
return;
}
world.setAccessible(true);
final Field entity = findDeclaredField(clickEvent.getPlayer().getClass(), "entity");
if (entity == null) {
Bukkit.getLogger().warning(InvTweaksPlugin.LOG_PLUGIN_NAME + " Cannot find entity field for players: ghost click will not function!");
return;
}
entity.setAccessible(true);
final Class<?> enumHand = interact.getParameterTypes()[2];
final Field enumHandA = enumHand.getDeclaredField("a");
enumHandA.setAccessible(true);
final Class<?> movingObjectPositionBlock = interact.getParameterTypes()[3];
final Method mopb_a = findDeclaredMethod(movingObjectPositionBlock, "a", 3);
if (mopb_a == null) {
Bukkit.getLogger().warning(InvTweaksPlugin.LOG_PLUGIN_NAME + " Cannot find movingObjectPositionBlock function: ghost click will not function!");
return;
}
mopb_a.setAccessible(true);
final Constructor<?> newVec3D = mopb_a.getParameterTypes()[0].getConstructor(double.class, double.class, double.class);
newVec3D.setAccessible(true);
final Field enumDirection = findDeclaredField(mopb_a.getParameterTypes()[1], "e");
if (enumDirection == null) {
Bukkit.getLogger().warning(InvTweaksPlugin.LOG_PLUGIN_NAME + " Cannot find EnumDirection value 'e': ghost click will not function!");
return;
}
enumDirection.setAccessible(true);
final Constructor<?> newBlockPosition = mopb_a.getParameterTypes()[2].getDeclaredConstructor(int.class, int.class, int.class);
newBlockPosition.setAccessible(true);
interact.invoke(
nmsBlock,
world.get(b),
entity.get(clickEvent.getPlayer()),
enumHandA.get(null),
mopb_a.invoke(
null,
newVec3D.newInstance(
clickEvent.getPlayer().getLocation().getX(),
clickEvent.getPlayer().getLocation().getY(),
clickEvent.getPlayer().getLocation().getZ()
),
enumDirection.get(null),
newBlockPosition.newInstance(b.getX(), b.getY(), b.getZ())
)
);
} catch (IllegalAccessException | InvocationTargetException | NoSuchFieldException | NoSuchMethodException | InstantiationException e) {
e.printStackTrace();
}
}
}
}
private static Method findDeclaredMethod(final Class<?> in, final String name, final int paramLen) {
for (final Method check : in.getDeclaredMethods())
if (check.getParameterTypes().length == paramLen && check.getName().equals(name))
return check;
return null;
}
private static Method findDeclaredMethod(Class<?> start, final String name) {
do {
for (final Method check : start.getDeclaredMethods())
if (check.getName().equals(name))
return check;
start = start.getSuperclass();
} while(start != null);
return null;
}
private static Field findDeclaredField(Class<?> start, final String name) {
do {
for (final Field check : start.getDeclaredFields())
if (check.getName().equals(name))
return check;
start = start.getSuperclass();
} while(start != null);
return null;
}
}

View File

@ -0,0 +1,73 @@
package dev.w1zzrd.invtweaks.listener;
import dev.w1zzrd.invtweaks.feature.NamedChestManager;
import org.bukkit.Chunk;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public final class PlayerMoveRenderListener implements Listener {
public static final int RENDER_RADIUS = 3;
private final List<Chunk> trackers = new ArrayList<>();
private final List<UUID> tracked = new ArrayList<>();
private final NamedChestManager manager;
public PlayerMoveRenderListener(final NamedChestManager manager) {
this.manager = manager;
}
@EventHandler
public void onPlayerMove(final PlayerMoveEvent event) {
final int index = Collections.binarySearch(tracked, event.getPlayer().getUniqueId());
final Player who = event.getPlayer();
final Chunk chunk = who.getLocation().getChunk();
if (index < 0) {
final int actualIndex = -(index + 1);
trackers.add(actualIndex, chunk);
tracked.add(actualIndex, who.getUniqueId());
triggerRender(who);
}
else if (!trackers.get(index).equals(event.getPlayer().getLocation().getChunk())) {
trackers.set(index, chunk);
triggerRender(event.getPlayer());
}
}
@EventHandler
public void onPlayerJoin(final PlayerJoinEvent event) {
final int index = Collections.binarySearch(tracked, event.getPlayer().getUniqueId());
// Should always be true
if (index < 0) {
trackers.add(-(index + 1), event.getPlayer().getLocation().getChunk());
tracked.add(-(index + 1), event.getPlayer().getUniqueId());
triggerRender(event.getPlayer());
}
}
@EventHandler
public void onPlayerLeave(final PlayerQuitEvent event) {
final int index = Collections.binarySearch(tracked, event.getPlayer().getUniqueId());
// Should always be true
if (index >= 0) {
trackers.remove(index);
tracked.remove(index);
manager.untrackPlayer(event.getPlayer());
}
}
private void triggerRender(final Player player) {
manager.renderTags(player, RENDER_RADIUS);
}
}

View File

@ -0,0 +1,21 @@
package dev.w1zzrd.invtweaks.listener;
import dev.w1zzrd.spigot.wizcompat.packet.Players;
import org.bukkit.block.Sign;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import java.util.Objects;
public class SignEditListener implements Listener {
@EventHandler
public void onSignClick(final PlayerInteractEvent event) {
if(event.getAction() == Action.RIGHT_CLICK_BLOCK &&
Objects.requireNonNull(event.getClickedBlock()).getState() instanceof Sign &&
event.getPlayer().isSneaking()) { // Sneak-right-click to edit sign
Players.openSignEditor(event.getPlayer(), event.getClickedBlock().getLocation());
}
}
}

View File

@ -2,13 +2,17 @@ package dev.w1zzrd.invtweaks.listener;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.entity.ThrowableProjectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.player.PlayerItemBreakEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.plugin.Plugin;
import java.util.logging.Logger;
@ -19,6 +23,12 @@ import static dev.w1zzrd.invtweaks.InvTweaksPlugin.LOG_PLUGIN_NAME;
*/
public class StackReplaceListener implements Listener {
private final Plugin plugin;
public StackReplaceListener(final Plugin plugin) {
this.plugin = plugin;
}
/**
* Max index for main player inventory
*/
@ -51,6 +61,36 @@ public class StackReplaceListener implements Listener {
logger.fine(LOG_PLUGIN_NAME + " Moved tool into empty hand for player " + event.getPlayer().getName());
}
@EventHandler
public void onPlayerThrowSnowballEvent(final ProjectileLaunchEvent event) {
if (event.getEntity() instanceof ThrowableProjectile &&
event.getEntity().getShooter() instanceof Player) {
final ThrowableProjectile projectile = (ThrowableProjectile) event.getEntity();
final Player thrower = (Player) event.getEntity().getShooter();
assert thrower != null;
final PlayerInventory inventory = thrower.getInventory();
final ItemStack stack = projectile.getItem();
final EquipmentSlot slot;
if (inventory.getItemInMainHand().getType() == stack.getType())
slot = EquipmentSlot.HAND;
else if (inventory.getItemInOffHand().getType() == stack.getType())
slot = EquipmentSlot.OFF_HAND;
else return;
if (inventory.getItem(slot).getAmount() == 1)
Bukkit.getScheduler().runTaskLater(
plugin,
() -> {
if (findAndMoveSimilarStack(stack, slot, inventory, CompareFunc.defaultFunc()))
logger.fine(LOG_PLUGIN_NAME + " Moved snowball into empty hand for player " + thrower.getName());
},
1);
}
}
/**
* Attempt to find and move a similar stack in the inventory to the one given, using the supplied comparison
* function

View File

@ -0,0 +1,127 @@
package dev.w1zzrd.invtweaks.listener;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.server.TabCompleteEvent;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Listener for providing tab completions for all commands in this plugin
*/
public class TabCompletionListener implements Listener {
private static final List<NamespacedKey> materialTypes = Arrays.stream(Material.values())
.map(Material::getKey)
.sorted(Comparator.comparing(NamespacedKey::toString))
.collect(Collectors.toList());
/**
* Whether or not there are namespaces other than the default "minecraft"
*/
private static final boolean multiNS;
static {
String ns = null;
for (final Material mat : Material.values()) {
if (ns == null) ns = mat.getKey().getNamespace();
else if (!ns.equals(mat.getKey().getNamespace())) {
ns = null;
break;
}
}
multiNS = ns == null;
}
@EventHandler
public void onTabCompleteEvent(final TabCompleteEvent event) {
if (event.getSender() instanceof Player) {
final String buffer = event.getBuffer();
final List<String> completions = event.getCompletions();
if (buffer.startsWith("/search ")) {
final String[] split = buffer.split(" ");
if (split.length > 2) {
completions.clear();
event.setCancelled(true);
} else if (split.length == 2) {
completions.addAll(getMatching(split[1]).collect(Collectors.toList()));
} else {
completions.clear();
completions.addAll(materialTypes.stream().map(TabCompletionListener::toUniqueNSString).collect(Collectors.toList()));
}
} else if (buffer.startsWith("/magnet ")) {
completions.clear();
} else if (buffer.startsWith("/sort ")) {
completions.clear();
}
}
}
public static String toUniqueNSString(final NamespacedKey key) {
return multiNS ? key.toString() : key.getKey();
}
/**
* Get all material namespace names which fuzzy-match the given string
* @param arg String to match to materials
* @return Stream of namespace strings associated with matched materials
*/
public static Stream<String> getMatching(final String arg) {
final String[] key = arg.split(":", 2);
return Arrays.stream(Material.values())
.filter(it -> (key.length == 1 || it.getKey().getNamespace().equals(key[0])) &&
it.getKey().getKey().contains(key[key.length - 1]))
.map(Material::getKey)
.map(it -> key.length == 1 ? it.getKey() : it.toString());
}
/**
* Get all materials which fuzzy-match the given string
* @param arg String to match to materials
* @return Stream of matched materials
*/
public static Stream<Material> getAllMaterialsMatching(final String arg) {
final String[] key = arg.split(":", 2);
return Arrays.stream(Material.values())
.filter(it -> (key.length == 1 || it.getKey().getNamespace().equals(key[0])) &&
it.getKey().getKey().contains(key[key.length - 1]));
}
/**
* Find a {@link Material} which is uniquely identifiable by the given string
* @param arg Part of namespaced key which uniquely identifies the material
* @return The identified material, if any, else null
*/
public static Material getMaterialMatching(final String arg) {
final List<Material> mats = getAllMaterialsMatching(arg).collect(Collectors.toList());
if (mats.size() == 0)
return null;
if (mats.size() == 1)
return mats.get(0);
return mats.stream()
.filter(it -> multiNS || arg.contains(":") ? arg.equals(it.getKey().toString()) : arg.equals(it.getKey().getKey()))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,257 @@
package dev.w1zzrd.invtweaks.listener;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.errorMessage;
import static org.bukkit.Material.*;
public class TreeCapitatorListener implements Listener {
private static final int MAX_SEARCH_BLOCKS = 69420;
private static final List<Material> targetMaterials = Arrays.asList(
ACACIA_LOG,
OAK_LOG,
BIRCH_LOG,
JUNGLE_LOG,
DARK_OAK_LOG,
SPRUCE_LOG,
CRIMSON_STEM,
WARPED_STEM,
ACACIA_WOOD,
OAK_WOOD,
BIRCH_WOOD,
JUNGLE_WOOD,
DARK_OAK_WOOD,
SPRUCE_WOOD,
CRIMSON_STEM,
WARPED_STEM,
CRIMSON_HYPHAE,
WARPED_HYPHAE,
STRIPPED_ACACIA_LOG,
STRIPPED_OAK_LOG,
STRIPPED_BIRCH_LOG,
STRIPPED_JUNGLE_LOG,
STRIPPED_DARK_OAK_LOG,
STRIPPED_SPRUCE_LOG,
STRIPPED_CRIMSON_STEM,
STRIPPED_WARPED_STEM,
STRIPPED_CRIMSON_HYPHAE,
STRIPPED_WARPED_HYPHAE,
ACACIA_LEAVES,
OAK_LEAVES,
BIRCH_LEAVES,
JUNGLE_LEAVES,
DARK_OAK_LEAVES,
SPRUCE_LEAVES,
NETHER_WART_BLOCK,
WARPED_WART_BLOCK,
SHROOMLIGHT
);
private static final List<Material> leaves = Arrays.asList(
ACACIA_LEAVES,
OAK_LEAVES,
BIRCH_LEAVES,
JUNGLE_LEAVES,
DARK_OAK_LEAVES,
SPRUCE_LEAVES,
NETHER_WART_BLOCK,
WARPED_WART_BLOCK,
SHROOMLIGHT
);
private final Enchantment capitatorEnchantment;
private final double hungerPerBlock;
private final int minHunger;
public TreeCapitatorListener(final Enchantment capitatorEnchantment, final double hungerPerBlock, final int minHunger) {
this.capitatorEnchantment = capitatorEnchantment;
this.hungerPerBlock = hungerPerBlock;
this.minHunger = minHunger;
}
@EventHandler
public void onBlockBreak(final BlockBreakEvent event) {
final ItemStack handTool = event.getPlayer().getInventory().getItemInMainHand();
if (event.isCancelled() || !handTool.containsEnchantment(capitatorEnchantment))
return;
// Check if capitator functionality is prevented by hunger
if (event.getPlayer().getFoodLevel() < minHunger) {
event.getPlayer().spigot().sendMessage(errorMessage("You are too tired to fell the tree"));
return;
}
if (handTool.getItemMeta() instanceof final Damageable tool) {
if (!leaves.contains(event.getBlock().getType()) && targetMaterials.contains(event.getBlock().getType())) {
int logBreakCount = 0;
for (final Block found : findAdjacent(event.getBlock(), getMaxUses(handTool, tool)))
if (event.getPlayer().getGameMode() == GameMode.CREATIVE)
found.setType(AIR);
else {
if (!leaves.contains(found.getType()))
++logBreakCount;
found.breakNaturally(handTool);
}
if (event.getPlayer().getGameMode() != GameMode.CREATIVE && !applyDamage(handTool, logBreakCount)) {
event.getPlayer().getInventory().setItemInMainHand(new ItemStack(AIR));
event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f);
}
if (event.getPlayer().getGameMode() == GameMode.ADVENTURE || event.getPlayer().getGameMode() == GameMode.SURVIVAL) {
final int hunger = (int) Math.round(hungerPerBlock * logBreakCount);
event.getPlayer().setFoodLevel(Math.max(0, event.getPlayer().getFoodLevel() - hunger));
}
event.setCancelled(true);
}
}
}
private static int getMaxUses(final ItemStack stack, final Damageable tool) {
return ((stack.getEnchantmentLevel(Enchantment.DURABILITY) + 1) * (stack.getType().getMaxDurability()) - tool.getDamage()) + 1;
}
private static boolean applyDamage(final ItemStack stack, final int brokenBlocks) {
final ItemMeta meta = stack.getItemMeta();
if (meta instanceof final Damageable toolMeta) {
final int unbreakingDivider = stack.getEnchantmentLevel(Enchantment.DURABILITY) + 1;
final int round = brokenBlocks % unbreakingDivider;
final int dmg = (brokenBlocks / unbreakingDivider) + (round != 0 ? 1 : 0);
if (dmg > (stack.getType().getMaxDurability() - toolMeta.getDamage()))
return false;
toolMeta.setDamage(toolMeta.getDamage() + dmg);
stack.setItemMeta(meta);
}
return true;
}
private static List<Block> findAdjacent(final Block start, final int softMax) {
List<Block> frontier = new ArrayList<>();
final List<Block> matches = new ArrayList<>();
frontier.add(start);
matches.add(start);
int total = 1;
int softMaxCount = 1;
// Keep finding blocks until we have no new matches
while (frontier.size() > 0 && total < MAX_SEARCH_BLOCKS && softMaxCount < softMax) {
final long result = addAdjacents(frontier, matches, total, softMaxCount, softMax);
total = (int) (result >>> 32);
softMaxCount = (int) (result & 0xFFFFFFFFL);
}
return matches;
}
private static long addAdjacents(final List<Block> frontier, final List<Block> collect, int total, int softMax, final int softMaxCap) {
final List<Block> newFrontier = new ArrayList<>();
OUTER:
for (final Block check : frontier)
for (int x = -1; x <= 1; ++x)
for (int y = -1; y <= 1; ++y)
for (int z = -1; z <= 1; ++z)
if ((x | y | z) != 0) {
final Block offset = offset(collect, check, x, y, z);
if (offset != null && !binaryContains(newFrontier, offset)) {
binaryInsert(collect, offset);
binaryInsert(newFrontier, offset);
if (!leaves.contains(offset.getType())) {
++softMax;
if (softMax >= softMaxCap)
break OUTER;
}
}
// Short-circuit for max search
++total;
if (total == MAX_SEARCH_BLOCKS)
break OUTER;
}
frontier.clear();
frontier.addAll(newFrontier);
return (((long)total) << 32) | (((long) softMax) & 0xFFFFFFFFL);
}
private static Block offset(final List<Block> checked, final Block source, int x, int y, int z) {
final Block offset = source.getWorld().getBlockAt(source.getLocation().add(x, y, z));
if (targetMaterials.contains(offset.getType()) && (!leaves.contains(source.getType()) || !leaves.contains(offset.getType())) && !binaryContains(checked, offset))
return offset;
return null;
}
private static boolean binaryContains(final List<Block> collection, final Block find) {
return binarySearchBlock(collection, find) >= 0;
}
private static void binaryInsert(final List<Block> collection, final Block insert) {
final int index = binarySearchBlock(collection, insert);
if (index >= 0)
return;
collection.add(-(index + 1), insert);
}
private static void binaryRemove(final List<Block> collection, final Block remove) {
final int index = binarySearchBlock(collection, remove);
if (index < 0)
return;
collection.remove(index);
}
private static int binarySearchBlock(final List<Block> collection, final Block find) {
return Collections.binarySearch(collection, find, TreeCapitatorListener::blockCompare);
}
private static int blockCompare(final Block b1, final Block b2) {
return coordinateCompare(b1.getX(), b1.getY(), b1.getZ(), b2.getX(), b2.getY(), b2.getZ());
}
private static int coordinateCompare(final int x1, final int y1, final int z1, final int x2, final int y2, final int z2) {
final int x = Integer.compare(x1, x2);
if (x != 0)
return x;
final int y = Integer.compare(y1, y2);
if (y != 0)
return y;
return Integer.compare(z1, z2);
}
}

View File

@ -9,6 +9,8 @@
* /magnet Toggle magnetism for the sender. When magnetism is enabled, all
* items within a 16x16x16 cube around the player that can be picked
* up are teleported to the player relatively frequently
* /search Find a given item type in any nearby chest and open the chest for
* the player
* </pre>
* <h3>Features</h3>
* <ul>

View File

@ -0,0 +1,367 @@
package dev.w1zzrd.invtweaks.serialization;
import dev.w1zzrd.spigot.wizcompat.serialization.SimpleReflectiveConfigItem;
import org.bukkit.Location;
import java.util.*;
import java.util.stream.Stream;
public class ChestNameConfig extends SimpleReflectiveConfigItem {
private List<ChestNameWorldEntry> worldEntries;
public ChestNameConfig(Map<String, Object> mappings) {
super(mappings);
}
public ChestNameConfig() {
this(Collections.emptyMap());
worldEntries = new ArrayList<>();
}
public ChestNameWorldEntry getEntry(final UUID worldID, final boolean addIfMissing) {
final int index = indexOf(worldID);
if (index >= 0)
return worldEntries.get(index);
else if (addIfMissing) {
final ChestNameWorldEntry entry = new ChestNameWorldEntry(worldID);
worldEntries.add(-(index + 1), entry);
return entry;
}
return null;
}
public ChestNameWorldEntry getEntry(final UUID worldID) {
return getEntry(worldID, true);
}
public ChestNameWorldEntry.ChestNameChunkEntry getChunkEntry(final UUID worldID, final int chunkX, final int chunkZ) {
final int index = indexOf(worldID);
if (index >= 0)
return worldEntries.get(index).getChunk(chunkX, chunkZ);
return null;
}
public ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry getEntryAt(final UUID worldID, final Location location) {
final ChestNameWorldEntry.ChestNameChunkEntry chunk = getChunkEntry(worldID, location.getBlockX() >> 4, location.getBlockZ() >> 4);
if (chunk != null) {
return chunk.getEntry(location.getBlockX(), location.getBlockY(), location.getBlockZ(), false);
}
return null;
}
public boolean contains(final UUID worldID) {
return getEntry(worldID) != null;
}
public void add(final UUID worldID) {
getEntry(worldID);
}
public void remove(final UUID worldID) {
final int index = indexOf(worldID);
if (index >= 0)
worldEntries.remove(index);
}
private int indexOf(final UUID worldID) {
return Collections.binarySearch(worldEntries, new ChestNameWorldEntry(worldID));
}
public void deleteEmptyWorld(final ChestNameWorldEntry world) {
if (!world.hasEntries()) {
final int index = Collections.binarySearch(worldEntries, world);
if (index >= 0)
worldEntries.remove(index);
}
}
public static final class ChestNameWorldEntry extends SimpleReflectiveConfigItem implements Comparable<ChestNameWorldEntry> {
private String worldIDStr;
private final transient UUID worldID;
private List<ChestNameChunkEntry> chunks;
/**
* Required constructor for deserializing data
*
* @param mappings Data to deserialize
*/
public ChestNameWorldEntry(Map<String, Object> mappings) {
super(mappings);
worldID = UUID.fromString(worldIDStr);
}
ChestNameWorldEntry(final UUID worldID) {
super(Collections.emptyMap());
this.worldID = worldID;
worldIDStr = worldID.toString();
chunks = new ArrayList<>();
}
public UUID getWorldID() {
return worldID;
}
@Override
public int compareTo(final ChestNameWorldEntry o) {
return getWorldID().compareTo(o.getWorldID());
}
public String getName(final Location location) {
final ChestNameChunkEntry chunk = getChunk(location, false);
if (chunk == null)
return null;
else
return chunk.getName(location);
}
public boolean contains(final Location location) {
return getName(location) != null;
}
public boolean hasEntries() {
return chunks.size() > 0;
}
public void add(final Location location, final String name) {
Objects.requireNonNull(getChunk(location, true)).add(location, name);
}
public void remove(final Location location) {
final int index = indexOf(location);
if (index >= 0)
chunks.remove(index);
}
private int indexOf(final Location location) {
return Collections.binarySearch(chunks, new ChestNameChunkEntry(location));
}
private int indexOf(final int chunkX, final int chunkZ) {
return Collections.binarySearch(chunks, new ChestNameChunkEntry(chunkX, chunkZ));
}
private ChestNameChunkEntry getChunk(final Location location, final boolean addIfMissing) {
final int index = indexOf(location);
if (index >= 0)
return chunks.get(index);
else if (addIfMissing) {
final ChestNameChunkEntry entry = new ChestNameChunkEntry(location);
chunks.add(-(index + 1), entry);
return entry;
}
return null;
}
private ChestNameChunkEntry getChunk(final int chunkX, final int chunkZ) {
final int index = indexOf(chunkX, chunkZ);
if (index >= 0)
return chunks.get(index);
else
return null;
}
private void clearChunk(final Location location) {
final int index = indexOf(location);
if (index >= 0)
chunks.remove(index);
}
public void deleteEmptyChunk(final ChestNameChunkEntry chunk) {
if (!chunk.hasEntries()) {
final int index = Collections.binarySearch(chunks, chunk);
if (index >= 0)
chunks.remove(index);
}
}
public static final class ChestNameChunkEntry extends SimpleReflectiveConfigItem implements Comparable<ChestNameChunkEntry> {
private int x, z;
private List<ChestNameConfigEntry> entries;
private transient boolean dirty = false;
public ChestNameChunkEntry(Map<String, Object> mappings) {
super(mappings);
}
ChestNameChunkEntry(final int x, final int z) {
super(Collections.emptyMap());
this.x = x;
this.z = z;
entries = new ArrayList<>();
}
ChestNameChunkEntry(final Location location) {
// Convert world coordinates to chunk coordinates
this(location.getBlockX() >> 4, location.getBlockZ() >> 4);
}
public int getX() {
return x;
}
public int getZ() {
return z;
}
public boolean isDirty() {
return dirty;
}
public void setDirty(final boolean dirty) {
this.dirty = dirty;
}
public boolean hasEntries() {
return entries.size() > 0;
}
public void add(final int x, final int y, final int z, final String name) {
final ChestNameConfigEntry check = getEntry(x, y, z, true);
assert check != null;
check.setName(name);
setDirty(true);
}
public void add(final Location location, final String name) {
add(location.getBlockX(), location.getBlockY(), location.getBlockZ(), name);
}
public String getName(final Location location) {
final ChestNameConfigEntry entry = getEntry(location.getBlockX(), location.getBlockY(), location.getBlockZ(), false);
if (entry == null)
return null;
else
return entry.getName();
}
private ChestNameConfigEntry getEntry(final int x, final int y, final int z, final boolean createIfMissing) {
final ChestNameConfigEntry find = new ChestNameConfigEntry(x, y, z);
final int index = indexOf(find);
if (index >= 0)
return entries.get(index);
else if (createIfMissing) {
entries.add(-(index + 1), find);
return find;
}
return null;
}
public void removeEntry(final ChestNameConfigEntry entry) {
final int index = indexOf(entry);
if (index >= 0) {
entries.remove(index);
setDirty(true);
}
}
public Stream<ChestNameConfigEntry> streamEntries() {
return entries.stream();
}
private int indexOf(final ChestNameConfigEntry find) {
return Collections.binarySearch(entries, find);
}
@Override
public int compareTo(final ChestNameChunkEntry o) {
final int compX = Integer.compare(x, o.x);
if (compX == 0)
return Integer.compare(z, o.z);
return compX;
}
public static final class ChestNameConfigEntry extends SimpleReflectiveConfigItem implements Comparable<ChestNameConfigEntry> {
private transient Object entity;
private transient int locInt;
private String loc;
private String name;
public ChestNameConfigEntry(Map<String, Object> mappings) {
super(mappings);
locInt = Integer.parseInt(loc, 16);
}
ChestNameConfigEntry(final int x, final int y, final int z, final String name) {
super(Collections.emptyMap());
locInt = ((y & 0xFFF) << 8) | ((x & 0xF) << 4) | (z & 0xF);
loc = Integer.toString(locInt, 16);
this.name = name;
}
ChestNameConfigEntry(final int x, final int y, final int z) {
this(x, y, z, null);
}
ChestNameConfigEntry(final Location location, final String name) {
this(location.getBlockX() & 0xF, location.getBlockY(), location.getBlockZ() & 0xF, name);
}
ChestNameConfigEntry(final Location location) {
this(location, null);
}
public Object getEntity(final EntityCreator creator) {
if (entity == null)
entity = creator.createFakeEntity();
return entity;
}
public int getChunkX() {
return (locInt >>> 4) & 0xF;
}
public int getChunkZ() {
return locInt & 0xF;
}
public int getY() {
return (locInt >>> 8) & 0xFFF;
}
public String getName() {
return name;
}
void setName(final String name) {
this.name = name;
}
@Override
public int compareTo(final ChestNameConfigEntry o) {
return Integer.compare(locInt, o.locInt);
}
public interface EntityCreator {
Object createFakeEntity();
}
}
}
}
}

View File

@ -1,5 +1,6 @@
package dev.w1zzrd.invtweaks.serialization;
import dev.w1zzrd.spigot.wizcompat.serialization.SimpleReflectiveConfigItem;
import org.bukkit.plugin.Plugin;
import java.util.Map;

View File

@ -1,5 +1,7 @@
package dev.w1zzrd.invtweaks.serialization;
import dev.w1zzrd.spigot.wizcompat.serialization.SimpleReflectiveConfigItem;
import dev.w1zzrd.spigot.wizcompat.serialization.UUIDList;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

View File

@ -0,0 +1,31 @@
package dev.w1zzrd.invtweaks.serialization;
import dev.w1zzrd.spigot.wizcompat.serialization.SimpleReflectiveConfigItem;
import java.util.Map;
public class SearchConfig extends SimpleReflectiveConfigItem {
private int searchRadiusX, searchRadiusY, searchRadiusZ;
/**
* Required constructor for deserializing data
*
* @param mappings Data to deserialize
*/
public SearchConfig(Map<String, Object> mappings) {
super(mappings);
}
public int getSearchRadiusX() {
return searchRadiusX;
}
public int getSearchRadiusY() {
return searchRadiusY;
}
public int getSearchRadiusZ() {
return searchRadiusZ;
}
}

View File

@ -1,126 +0,0 @@
package dev.w1zzrd.invtweaks.serialization;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Configuration serializable type for automatically serializing/deserializing fields
*/
public class SimpleReflectiveConfigItem implements ConfigurationSerializable {
/**
* Required constructor for deserializing data
* @param mappings Data to deserialize
*/
public SimpleReflectiveConfigItem(final Map<String, Object> mappings) {
deserializeMapped(mappings);
}
@Override
public Map<String, Object> serialize() {
final HashMap<String, Object> values = new HashMap<>();
Arrays.stream(getClass().getDeclaredFields())
.filter(it -> !Modifier.isTransient(it.getModifiers()) && !Modifier.isStatic(it.getModifiers()))
.forEach(it -> {
try {
it.setAccessible(true);
values.put(it.getName(), it.get(this));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
return values;
}
/**
* Deserialize mapped data by name
* @param mappings Data to deserialize
*/
private void deserializeMapped(final Map<String, Object> mappings) {
for (final Field field : getClass().getDeclaredFields()) {
if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers()))
continue;
try {
// Try to find mappings by field name
if (mappings.containsKey(field.getName()))
parse(mappings.get(field.getName()), field, this);
} catch (IllegalAccessException | InvocationTargetException e) {
// This shouldn't happen
e.printStackTrace();
}
}
}
/**
* Attempt to parse a value such that it can be stored in the given field
* @param value Value to parse
* @param field Field to store value in
* @param instance Configuration object for which to parse the vaue
* @throws IllegalAccessException Should never be thrown
* @throws InvocationTargetException Should never be thrown
*/
private static void parse(final Object value, final Field field, final Object instance) throws IllegalAccessException, InvocationTargetException {
field.setAccessible(true);
if (field.getType().isPrimitive() && value == null)
throw new NullPointerException("Attempt to assign null to a primitive field");
final Class<?> boxed = getBoxedType(field.getType());
if (boxed.isAssignableFrom(value.getClass())) {
field.set(instance, value);
return;
}
if (value instanceof String) {
final Method parser = locateParser(boxed, field.getType().isPrimitive() ? field.getType() : null);
if (parser != null)
field.set(instance, parser.invoke(null, value));
}
throw new IllegalArgumentException(String.format("No defined parser for value \"%s\"", value));
}
/**
* Converter for boxed primitives
* @param cls Primitive type
* @return Boxed type for primitive type, else the given type
*/
private static Class<?> getBoxedType(final Class<?> cls) {
if (cls == int.class) return Integer.class;
else if (cls == double.class) return Double.class;
else if (cls == long.class) return Long.class;
else if (cls == float.class) return Float.class;
else if (cls == char.class) return Character.class;
else if (cls == byte.class) return Byte.class;
else if (cls == short.class) return Short.class;
else if (cls == boolean.class) return Boolean.class;
else return cls;
}
/**
* Attempt to find a parser method for the given type
* @param cls Type to find parser for
* @param prim Primitive type find parser for (if applicable)
* @return Static method which accepts a {@link String} argument and returns the desired type
*/
private static Method locateParser(final Class<?> cls, final Class<?> prim) {
for (final Method method : cls.getDeclaredMethods()) {
final Class<?>[] params = method.getParameterTypes();
if ((method.getName().startsWith("parse" + cls.getSimpleName()) || method.getName().equals("fromString")) &&
Modifier.isStatic(method.getModifiers()) &&
method.getReturnType().equals(prim != null ? prim : cls) &&
params.length == 1 && params[0].equals(String.class))
method.setAccessible(true);
return method;
}
return null;
}
}

View File

@ -1,47 +0,0 @@
package dev.w1zzrd.invtweaks.serialization;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import java.util.*;
import java.util.stream.Collectors;
/**
* Serializable dataclass holding a collection of {@link UUID} objects
*/
public class UUIDList implements ConfigurationSerializable {
/**
* This is public to decrease performance overhead
*/
public final List<UUID> uuids;
/**
* Wrap a backing list of {@link UUID} objects to enable configuration serialization
* @param backingList Modifiable, backing list of {@link UUID} objects
*/
public UUIDList(final List<UUID> backingList) {
uuids = backingList;
}
/**
* Create a blank list of {@link UUID} objects
*/
public UUIDList() {
this(new ArrayList<>());
}
/**
* Deserialize serialized UUID strings
* @param values Data to deserialize
*/
public UUIDList(final Map<String, Object> values) {
this();
if (values.containsKey("values"))
uuids.addAll(((Collection<String>)values.get("values")).stream().map(UUID::fromString).collect(Collectors.toSet()));
}
@Override
public Map<String, Object> serialize() {
return Collections.singletonMap("values", uuids.stream().map(UUID::toString).collect(Collectors.toList()));
}
}