Adds customization and automatic movement to crafting stations
All checks were successful
KnarCraft/BlacksmithVisuals/pipeline/head This commit looks good

This commit is contained in:
Kristian Knarvik 2024-08-03 17:32:36 +02:00
parent b1dab9b2cf
commit 87fa2f7c64
15 changed files with 1133 additions and 157 deletions

View File

@ -12,7 +12,7 @@
<name>BlacksmithVisuals</name> <name>BlacksmithVisuals</name>
<properties> <properties>
<java.version>1.8</java.version> <java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@ -84,7 +84,7 @@
<dependency> <dependency>
<groupId>net.knarcraft</groupId> <groupId>net.knarcraft</groupId>
<artifactId>blacksmith</artifactId> <artifactId>blacksmith</artifactId>
<version>1.1.1-SNAPSHOT</version> <version>1.1.2-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -1,149 +0,0 @@
package net.knarcraft.blacksmithvisuals;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.event.ActionStartEvent;
import net.knarcraft.blacksmith.event.BlacksmithReforgeFailEvent;
import net.knarcraft.blacksmith.event.BlacksmithReforgeStartEvent;
import net.knarcraft.blacksmith.event.BlacksmithReforgeSucceedEvent;
import net.knarcraft.blacksmith.event.NPCSoundEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageFailEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageStartEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageSucceedEvent;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitScheduler;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
/**
* A listener for blacksmith-related events
*/
public class BlacksmithListener implements Listener {
private static final Random random = new Random();
@EventHandler
public void onReforgeStart(@NotNull BlacksmithReforgeStartEvent event) {
onActionStart(event);
}
@EventHandler
public void onSalvageStart(@NotNull ScrapperSalvageStartEvent event) {
onActionStart(event);
}
@EventHandler
public void onDefaultSound(@NotNull NPCSoundEvent event) {
event.setCancelled(true);
}
@EventHandler
public void onReforgeSuccess(@NotNull BlacksmithReforgeSucceedEvent event) {
playSuccessSound(event.getNpc().getEntity());
}
@EventHandler
public void onSalvageSuccess(@NotNull ScrapperSalvageSucceedEvent event) {
playSuccessSound(event.getNpc().getEntity());
}
@EventHandler
public void onReforgeFail(@NotNull BlacksmithReforgeFailEvent event) {
playFailSound(event.getNpc().getEntity());
}
@EventHandler
public void onSalvageFail(@NotNull ScrapperSalvageFailEvent event) {
playFailSound(event.getNpc().getEntity());
}
/**
* A method performing actions required when a blacksmith or scrapper action starts
*
* @param event <p>The event that's starting</p>
*/
private void onActionStart(@NotNull ActionStartEvent event) {
BukkitScheduler scheduler = Bukkit.getScheduler();
int playWorkSound = scheduler.scheduleSyncRepeatingTask(BlacksmithPlugin.getInstance(),
() -> displayWorkAnimation(event), 20, 5);
scheduler.scheduleSyncDelayedTask(BlacksmithPlugin.getInstance(), () -> scheduler.cancelTask(playWorkSound),
event.getActionDurationTicks());
}
/**
* Displays the animation of a working NPC
*
* @param event <p>The action start event to display for</p>
*/
private void displayWorkAnimation(@NotNull ActionStartEvent event) {
if (random.nextInt(100) >= 20) {
return;
}
this.playWorkSound(event.getNpc().getEntity());
ProtocolManager manager = ProtocolLibrary.getProtocolManager();
PacketContainer packet = manager.createPacket(PacketType.Play.Server.ANIMATION);
packet.getIntegers().write(0, event.getNpc().getEntity().getEntityId());
packet.getIntegers().write(1, 3);
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getWorld().equals(event.getNpc().getEntity().getWorld())) {
manager.sendServerPacket(player, packet);
}
}
}
/**
* Plays the fail action sound
*
* @param entity <p>The entity to play the sound at</p>
*/
private void playFailSound(@NotNull Entity entity) {
playSound(entity, Sound.ENTITY_VILLAGER_NO);
}
/**
* Plays the succeed action sound
*
* @param entity <p>The entity to play the sound at</p>
*/
private void playSuccessSound(@NotNull Entity entity) {
playSound(entity, Sound.BLOCK_ANVIL_USE);
}
/**
* Plays a npc sound using a cancellable event
*
* @param entity <p>The entity that should play the sound</p>
*/
private void playWorkSound(@NotNull Entity entity) {
playSound(entity, Sound.ITEM_ARMOR_EQUIP_NETHERITE);
}
/**
* Plays a npc sound using a cancellable event
*
* @param entity <p>The entity that should play the sound</p>
*/
private void playSound(@NotNull Entity entity, Sound sound) {
World world = entity.getLocation().getWorld();
if (world == null) {
return;
}
world.playSound(entity, sound, SoundCategory.AMBIENT, 0.5f, 1.0f);
}
}

View File

@ -1,6 +1,18 @@
package net.knarcraft.blacksmithvisuals; package net.knarcraft.blacksmithvisuals;
import net.knarcraft.blacksmithvisuals.command.ReloadCommand;
import net.knarcraft.blacksmithvisuals.command.SetNPCPositionCommand;
import net.knarcraft.blacksmithvisuals.listener.BlacksmithListener;
import net.knarcraft.blacksmithvisuals.manager.ConfigurationManager;
import net.knarcraft.blacksmithvisuals.manager.NPCDataManager;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.logging.Level;
/** /**
* The blacksmith visual main class * The blacksmith visual main class
@ -8,18 +20,102 @@ import org.bukkit.plugin.java.JavaPlugin;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class BlacksmithVisuals extends JavaPlugin { public final class BlacksmithVisuals extends JavaPlugin {
private static BlacksmithVisuals instance;
private ConfigurationManager configurationManager;
private NPCDataManager npcDataManager;
@Override @Override
public void onEnable() { public void onEnable() {
// Plugin startup logic BlacksmithVisuals.instance = this;
getServer().getPluginManager().registerEvents(new BlacksmithListener(), this);
//TODO: Allow customization of sounds, volumes and pitches //Copy default config to disk, and add missing configuration values
// Allow setting an idle position and a working position, and move the NPC to the right position at the right time. this.saveDefaultConfig();
// Use the distance between the two locations to decide how much sooner before the success/fail sounds are played the NPC should start walking this.getConfig().options().copyDefaults(true);
this.reloadConfig();
this.saveConfig();
try {
this.configurationManager = new ConfigurationManager(this.getConfig());
} catch (InvalidConfigurationException exception) {
this.getLogger().log(Level.SEVERE, "Could not properly load the configuration file. " +
"Please check it for errors!");
this.onDisable();
return;
}
try {
this.npcDataManager = NPCDataManager.load();
} catch (IOException e) {
this.getLogger().log(Level.SEVERE, "Could not properly load the data.yml file. " +
"Please check it for errors!");
this.onDisable();
return;
}
getServer().getPluginManager().registerEvents(new BlacksmithListener(this.configurationManager), this);
registerCommand("reload", new ReloadCommand());
registerCommand("setNPCPosition", new SetNPCPositionCommand());
} }
@Override @Override
public void onDisable() { public void onDisable() {
// Plugin shutdown logic // Plugin shutdown logic
} }
/**
* Reloads the configuration file
*/
public void reload() {
reloadConfig();
saveConfig();
try {
this.configurationManager.load(getConfig());
} catch (InvalidConfigurationException exception) {
this.getLogger().log(Level.SEVERE, "Could not properly load the configuration file. " +
"Please check it for errors!");
this.onDisable();
}
}
/**
* Registers a command
*
* @param commandName <p>The name of the command</p>
* @param executor <p>The executor to bind to the command</p>
*/
private void registerCommand(@NotNull String commandName, @NotNull CommandExecutor executor) {
PluginCommand command = this.getCommand(commandName);
if (command != null) {
command.setExecutor(executor);
}
}
/**
* Gets the NPC data manager
*
* @return <p>The NPC data manager</p>
*/
@NotNull
public NPCDataManager getNpcDataManager() {
return this.npcDataManager;
}
/**
* Gets the configuration manager
*
* @return <p>The configuration manager</p>
*/
@NotNull
public ConfigurationManager getConfigurationManager() {
return this.configurationManager;
}
/**
* Gets an instance of this plugin
*
* @return <p>An instance of this plugin</p>
*/
@NotNull
public static BlacksmithVisuals getInstance() {
return instance;
}
} }

View File

@ -0,0 +1,22 @@
package net.knarcraft.blacksmithvisuals.command;
import net.knarcraft.blacksmithvisuals.BlacksmithVisuals;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* The reload command
*/
public class ReloadCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] strings) {
BlacksmithVisuals.getInstance().reload();
commandSender.sendMessage("BlacksmithVisuals has been reloaded!");
return true;
}
}

View File

@ -0,0 +1,83 @@
package net.knarcraft.blacksmithvisuals.command;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.trait.ScrapperTrait;
import net.knarcraft.blacksmithvisuals.BlacksmithVisuals;
import net.knarcraft.blacksmithvisuals.container.NPCData;
import net.knarcraft.blacksmithvisuals.manager.NPCDataManager;
import net.knarcraft.blacksmithvisuals.property.NPCPosition;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* The commands for setting an NPC's positions
*/
public class SetNPCPositionCommand implements TabExecutor {
private static List<String> npcPositionNames = null;
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
NPC npc = CitizensAPI.getDefaultNPCSelector().getSelected(commandSender);
if (npc == null || (!npc.hasTrait(BlacksmithTrait.class) && !npc.hasTrait(ScrapperTrait.class))) {
commandSender.sendMessage("You must select an NPC before executing this command");
return true;
}
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be executed by a player");
return false;
}
if (arguments.length < 1) {
return false;
}
NPCPosition position = NPCPosition.fromInput(arguments[0]);
if (position == null) {
commandSender.sendMessage("Invalid position specified!");
return false;
}
NPCDataManager manager = BlacksmithVisuals.getInstance().getNpcDataManager();
NPCData data = manager.getData(npc.getUniqueId());
if (data == null) {
data = new NPCData(new HashMap<>());
}
data.positions().put(position, player.getLocation());
manager.putData(npc.getUniqueId(), data);
commandSender.sendMessage("Position " + position.getPositionName() + " set!");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
if (npcPositionNames == null) {
List<String> output = new ArrayList<>();
for (NPCPosition position : NPCPosition.values()) {
output.add(position.getPositionName());
}
npcPositionNames = output;
}
return npcPositionNames;
} else {
return List.of();
}
}
}

View File

@ -0,0 +1,57 @@
package net.knarcraft.blacksmithvisuals.container;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
/**
* An animation data container
*
* @param animateOffHand <p>Whether to animate the off-hand at all</p>
* @param animationDelay <p>The delay between each time the animation should be attempted (max swing rate)</p>
* @param animationChance <p>The probability of the animation triggering when the animation delay has passed</p>
*/
public record AnimationData(boolean animateOffHand, int animationDelay, int animationChance) {
/**
* Instantiates a new animation data object with sanity checks
*
* @param animateOffHand <p>Whether to animate the off-hand at all</p>
* @param animationDelay <p>The delay between each time the animation should be attempted (max swing rate)</p>
* @param animationChance <p>The probability of the animation triggering when the animation delay has passed</p>
*/
public AnimationData {
if (animationDelay < 1) {
animationDelay = 1;
}
if (animationChance < 1) {
animationChance = 1;
}
if (animationChance > 100) {
animationChance = 100;
}
}
/**
* Loads sound data from the given configuration key
*
* @param configuration <p>The configuration to load values from</p>
* @param rootKey <p>The key to load the data from</p>
* @return <p>The loaded sound data</p>
*/
@NotNull
public static AnimationData load(@NotNull FileConfiguration configuration,
@NotNull String rootKey) throws InvalidConfigurationException {
ConfigurationSection section = configuration.getConfigurationSection(rootKey);
if (section == null) {
throw new InvalidConfigurationException("Could not find the configuration section " + rootKey);
}
boolean animateOffHand = section.getBoolean("animateOffhand", true);
int animationDelay = section.getInt("animationDelay", 5);
int animationChance = section.getInt("animationChance", 20);
return new AnimationData(animateOffHand, animationDelay, animationChance);
}
}

View File

@ -0,0 +1,16 @@
package net.knarcraft.blacksmithvisuals.container;
import net.knarcraft.blacksmithvisuals.property.NPCPosition;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
/**
* Data for an NPC
*
* @param positions <p>A map storing all NPC positions</p>
*/
public record NPCData(@NotNull Map<NPCPosition, Location> positions) {
}

View File

@ -0,0 +1,108 @@
package net.knarcraft.blacksmithvisuals.container;
import net.knarcraft.blacksmithvisuals.BlacksmithVisuals;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.logging.Level;
/**
* A sound data container
*
* @param soundCategory <p>The category the sound should be played in</p>
* @param sound <p>The sound to be played</p>
* @param volume <p>The volume to play the sound at (> 0)</p>
* @param pitch <p>The pitch to play the sound at</p>
* @param offsetTicks <p>The amount of ticks to offset the sound by</p>
* @param enabled <p>Whether this sound is enabled</p>
*/
public record SoundData(@NotNull SoundCategory soundCategory, @NotNull Sound sound, float volume, float pitch,
int offsetTicks, boolean enabled) {
/**
* Instantiates a new sound data object with sanity checks
*
* @param soundCategory <p>The category the sound should be played in</p>
* @param sound <p>The sound to be played</p>
* @param volume <p>The volume to play the sound at (> 0)</p>
* @param pitch <p>The pitch to play the sound at</p>
* @param offsetTicks <p>The amount of ticks to offset the sound by</p>
* @param enabled <p>Whether this sound is enabled</p>
*/
public SoundData {
if (volume < 0) {
volume = 1;
}
if (pitch < 0) {
pitch = 0;
} else if (pitch > 1) {
pitch = 1;
}
Objects.requireNonNull(soundCategory);
Objects.requireNonNull(sound);
}
/**
* Loads sound data from the given configuration key
*
* @param configuration <p>The configuration to load values from</p>
* @param rootKey <p>The key to load the data from</p>
* @return <p>The loaded sound data</p>
*/
@NotNull
public static SoundData load(@NotNull FileConfiguration configuration,
@NotNull String rootKey) throws InvalidConfigurationException {
ConfigurationSection section = configuration.getConfigurationSection(rootKey);
if (section == null) {
throw new InvalidConfigurationException("Could not find the configuration section " + rootKey);
}
boolean enabled = section.getBoolean("enabled", true);
SoundCategory soundCategory = parseCategory(section.getString("soundCategory", "AMBIENT"));
Sound sound = parseSound(section.getString("sound", "ENTITY_PIG_DEATH"));
float pitch = (float) section.getDouble("pitch", 1);
float volume = (float) section.getDouble("volume", 1);
int offsetTicks = section.getInt("offset", 0);
return new SoundData(soundCategory, sound, volume, pitch, offsetTicks, enabled);
}
/**
* Safely parses a sound
*
* @param soundName <p>The name of the sound to parse</p>
* @return <p>The parsed sound</p>
*/
@NotNull
private static Sound parseSound(@NotNull String soundName) {
try {
return Sound.valueOf(soundName);
} catch (IllegalArgumentException exception) {
BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Invalid sound in configuration: " +
soundName);
return Sound.ENTITY_PIG_DEATH;
}
}
/**
* Safely parses a sound category
*
* @param categoryName <p>The name of the category to parse</p>
* @return <p>The parsed category</p>
*/
@NotNull
private static SoundCategory parseCategory(@NotNull String categoryName) {
try {
return SoundCategory.valueOf(categoryName);
} catch (IllegalArgumentException exception) {
BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Invalid sound category in " +
"configuration: " + categoryName);
return SoundCategory.AMBIENT;
}
}
}

View File

@ -0,0 +1,278 @@
package net.knarcraft.blacksmithvisuals.listener;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.event.ActionStartEvent;
import net.knarcraft.blacksmith.event.BlacksmithReforgeFailEvent;
import net.knarcraft.blacksmith.event.BlacksmithReforgeStartEvent;
import net.knarcraft.blacksmith.event.BlacksmithReforgeSucceedEvent;
import net.knarcraft.blacksmith.event.NPCSoundEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageFailEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageStartEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageSucceedEvent;
import net.knarcraft.blacksmithvisuals.BlacksmithVisuals;
import net.knarcraft.blacksmithvisuals.container.AnimationData;
import net.knarcraft.blacksmithvisuals.container.NPCData;
import net.knarcraft.blacksmithvisuals.container.SoundData;
import net.knarcraft.blacksmithvisuals.manager.ConfigurationManager;
import net.knarcraft.blacksmithvisuals.property.NPCPosition;
import net.knarcraft.blacksmithvisuals.property.SoundIdentifier;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitScheduler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Random;
import java.util.logging.Level;
/**
* A listener for blacksmith-related events
*/
public class BlacksmithListener implements Listener {
private final Random random;
private final ConfigurationManager configurationManager;
/**
* Instantiates a new blacksmith listener
*
* @param configurationManager <p>The configuration manager to get configuration values from</p>
*/
public BlacksmithListener(@NotNull ConfigurationManager configurationManager) {
this.random = new Random();
this.configurationManager = configurationManager;
}
@EventHandler
public void onDefaultSound(@NotNull NPCSoundEvent event) {
event.setCancelled(true);
}
@EventHandler
public void onReforgeStart(@NotNull BlacksmithReforgeStartEvent event) {
runWorkingAnimation(event, SoundIdentifier.REFORGING_WORKING, this.configurationManager.getBlacksmithAnimationData());
}
@EventHandler
public void onSalvageStart(@NotNull ScrapperSalvageStartEvent event) {
runWorkingAnimation(event, SoundIdentifier.SALVAGING_WORKING, this.configurationManager.getScrapperAnimationData());
}
@EventHandler
public void onReforgeSuccess(@NotNull BlacksmithReforgeSucceedEvent event) {
playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.REFORGING_SUCCESS));
}
@EventHandler
public void onSalvageSuccess(@NotNull ScrapperSalvageSucceedEvent event) {
playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.SALVAGING_SUCCESS));
}
@EventHandler
public void onReforgeFail(@NotNull BlacksmithReforgeFailEvent event) {
playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.REFORGING_FAILURE));
}
@EventHandler
public void onSalvageFail(@NotNull ScrapperSalvageFailEvent event) {
playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.SALVAGING_FAILURE));
}
/**
* Runs the working animation for an NPC
*
* @param event <p>The action that started</p>
* @param soundIdentifier <p>The identifier for the sound to play</p>
* @param animationData <p>The animation data for the animation to play</p>
*/
private void runWorkingAnimation(@NotNull ActionStartEvent event, @NotNull SoundIdentifier soundIdentifier,
@NotNull AnimationData animationData) {
BlacksmithVisuals instance = BlacksmithVisuals.getInstance();
BukkitScheduler scheduler = Bukkit.getScheduler();
NPC npc = event.getNpc();
long delay = moveToWorkingPosition(npc, NPCPosition.getFromMaterial(event.getCraftingStation()));
long finishTime = event.getActionDurationTicks() - (2 * delay);
scheduler.scheduleSyncDelayedTask(instance, () -> startWorkAnimation(npc.getEntity(),
this.configurationManager.getSoundData(soundIdentifier), animationData, finishTime - 20), delay);
scheduler.scheduleSyncDelayedTask(instance, () -> moveBack(event.getNpc()), finishTime);
}
/**
* Moves an NPC back to its idle position
*
* @param npc <p>The NPC to move</p>
*/
private void moveBack(@NotNull NPC npc) {
if (!npc.isSpawned()) {
return;
}
NPCData npcData = BlacksmithVisuals.getInstance().getNpcDataManager().getData(npc.getUniqueId());
if (npcData == null) {
return;
}
Location targetLocation = npcData.positions().get(NPCPosition.IDLE);
if (!npc.getNavigator().canNavigateTo(targetLocation)) {
BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Idle position for NPC " +
npc.getName() + " is unreachable!");
return;
}
npc.getNavigator().setTarget(targetLocation);
Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(), () ->
npc.getEntity().teleport(targetLocation), getWalkTime(npc.getEntity().getLocation(), targetLocation));
}
/**
* Moves a npc to its working position
*
* @param npc <p>The NPC to move</p>
* @param npcPosition <p>The npc position to move to</p>
* @return <p>The time the move will take</p>
*/
private long moveToWorkingPosition(@NotNull NPC npc, @Nullable NPCPosition npcPosition) {
if (!npc.isSpawned() || npcPosition == null) {
return 0;
}
NPCData npcData = BlacksmithVisuals.getInstance().getNpcDataManager().getData(npc.getUniqueId());
if (npcData == null) {
return 0;
}
Location targetLocation = npcData.positions().get(npcPosition);
if (targetLocation == null && npcPosition != NPCPosition.WORKING_REPAIRABLE) {
targetLocation = npcData.positions().get(NPCPosition.WORKING_REPAIRABLE);
}
if (targetLocation == null) {
return 0;
}
if (!npc.getNavigator().canNavigateTo(targetLocation)) {
BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Working position for NPC " +
npc.getName() + " is unreachable!");
return 0;
}
// Move NPC using Citizens path-finding
npc.getNavigator().setTarget(targetLocation);
// Teleport the NPC tp get it in the exact final location
long walkTime = getWalkTime(npc.getEntity().getLocation(), targetLocation);
Location finalTargetLocation = targetLocation;
Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(), () ->
npc.getEntity().teleport(finalTargetLocation), walkTime);
return walkTime;
}
/**
* Starts the working animation and sound
*
* @param entity <p>The entity to play the sound at</p>
* @param soundData <p>The sound data for the sound to play</p>
* @param animationData <p>The animation data for the animation to play</p>
* @param durationTicks <p>The duration of the work animation</p>
*/
private void startWorkAnimation(@NotNull Entity entity, @NotNull SoundData soundData,
@NotNull AnimationData animationData, long durationTicks) {
BlacksmithVisuals instance = BlacksmithVisuals.getInstance();
BukkitScheduler scheduler = Bukkit.getScheduler();
int playWorkSound = scheduler.scheduleSyncRepeatingTask(instance,
() -> animateNPC(entity, soundData, animationData), 20, animationData.animationDelay());
scheduler.scheduleSyncDelayedTask(instance, () -> scheduler.cancelTask(playWorkSound), durationTicks);
}
/**
* Gets the time it would take to go from one location to another in ticks
*
* @param startLocation <p>The location to start from</p>
* @param targetLocation <p>The target location</p>
* @return <p>The time in ticks</p>
*/
private long getWalkTime(@NotNull Location startLocation, @NotNull Location targetLocation) {
double distance = startLocation.distance(targetLocation);
double WALK_SPEED = 4.317;
return Math.round((distance / WALK_SPEED) * 20);
}
/**
* Animates a npc's hand, and plays a sound
*
* @param entity <p>The entity to animate and play the sound at</p>
* @param soundData <p>The sound to be played</p>
* @param animationData <p>The animation to be played</p>
*/
private void animateNPC(@NotNull Entity entity, @NotNull SoundData soundData, @NotNull AnimationData animationData) {
if (random.nextInt(100) >= animationData.animationChance()) {
return;
}
playSound(entity, soundData);
// Don't play disabled animations
if (!animationData.animateOffHand()) {
return;
}
if (soundData.offsetTicks() < 0) {
Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(),
() -> animateOffhand(entity), -soundData.offsetTicks());
} else {
animateOffhand(entity);
}
}
/**
* Animates an NPC's offhand
*
* @param entity <p>The entity to animate</p>
*/
private void animateOffhand(@NotNull Entity entity) {
ProtocolManager manager = ProtocolLibrary.getProtocolManager();
PacketContainer packet = manager.createPacket(PacketType.Play.Server.ANIMATION);
packet.getIntegers().write(0, entity.getEntityId());
packet.getIntegers().write(1, 3);
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getWorld().equals(entity.getWorld())) {
manager.sendServerPacket(player, packet);
}
}
}
/**
* Plays a sound according to the given sound data
*
* @param entity <p>The entity to play the sound from</p>
* @param soundData <p>The data describing the sound to play</p>
*/
private void playSound(@NotNull Entity entity, @NotNull SoundData soundData) {
// Don't play disabled sounds
if (!soundData.enabled()) {
return;
}
World world = entity.getLocation().getWorld();
if (world == null) {
return;
}
int delay = Math.max(soundData.offsetTicks(), 0);
Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(), () ->
world.playSound(entity, soundData.sound(), soundData.soundCategory(),
soundData.volume(), soundData.pitch()), delay);
}
}

View File

@ -0,0 +1,78 @@
package net.knarcraft.blacksmithvisuals.manager;
import net.knarcraft.blacksmithvisuals.container.AnimationData;
import net.knarcraft.blacksmithvisuals.container.SoundData;
import net.knarcraft.blacksmithvisuals.property.SoundIdentifier;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A manager keeping track of configuration options
*/
public class ConfigurationManager {
private Map<SoundIdentifier, SoundData> soundSettings;
private AnimationData scrapperAnimationData;
private AnimationData blacksmithAnimationData;
/**
* Instantiates a new configuration manager
*
* @param fileConfiguration <p>The file configuration to load values from</p>
* @throws InvalidConfigurationException <p>If the configuration file has missing or invalid values</p>
*/
public ConfigurationManager(@NotNull FileConfiguration fileConfiguration) throws InvalidConfigurationException {
load(fileConfiguration);
}
/**
* Loads the configuration from disk
*
* @param fileConfiguration <p>The file configuration to get values from</p>
* @throws InvalidConfigurationException <p>If the configuration file has missing or invalid values</p>
*/
public void load(@NotNull FileConfiguration fileConfiguration) throws InvalidConfigurationException {
soundSettings = new HashMap<>();
for (SoundIdentifier identifier : SoundIdentifier.values()) {
soundSettings.put(identifier, SoundData.load(fileConfiguration, identifier.getConfigNode()));
}
scrapperAnimationData = AnimationData.load(fileConfiguration, "scrapper.animation");
blacksmithAnimationData = AnimationData.load(fileConfiguration, "blacksmith.animation");
}
/**
* Gets the animation data for the scrapper's working animation
*
* @return <p>Scrapper animation data</p>
*/
@NotNull
public AnimationData getScrapperAnimationData() {
return this.scrapperAnimationData;
}
/**
* Gets the animation data for the blacksmith's working animation
*
* @return <p>Blacksmith animation data</p>
*/
@NotNull
public AnimationData getBlacksmithAnimationData() {
return this.blacksmithAnimationData;
}
/**
* Gets the sound data for the given identifier
*
* @param identifier <p>The identifier of the sound</p>
* @return <p>The sound's sound data</p>
*/
@NotNull
public SoundData getSoundData(@NotNull SoundIdentifier identifier) {
return soundSettings.get(identifier);
}
}

View File

@ -0,0 +1,115 @@
package net.knarcraft.blacksmithvisuals.manager;
import net.knarcraft.blacksmithvisuals.BlacksmithVisuals;
import net.knarcraft.blacksmithvisuals.container.NPCData;
import net.knarcraft.blacksmithvisuals.property.NPCPosition;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* A manager for NPC data
*/
public class NPCDataManager {
private final Map<UUID, NPCData> npcDataMap;
private static final File configurationFile = new File(BlacksmithVisuals.getInstance().getDataFolder(), "data.yml");
/**
* Instantiates a new NPC data manager
*
* @param npcDataMap <p>The map containing existing NPC data</p>
*/
public NPCDataManager(@NotNull Map<UUID, NPCData> npcDataMap) {
this.npcDataMap = npcDataMap;
}
/**
* Gets the NPC data for the NPC with the given id
*
* @param npcId <p>The id of the NPC to get data for</p>
* @return <p>The NPC data, or null if not set</p>
*/
@Nullable
public NPCData getData(@NotNull UUID npcId) {
return this.npcDataMap.get(npcId);
}
/**
* Adds new NPC data to an NPC
*
* @param npcId <p>The id of the NPC to add data to</p>
* @param npcData <p>The data to set</p>
*/
public void putData(@NotNull UUID npcId, @NotNull NPCData npcData) {
this.npcDataMap.put(npcId, npcData);
try {
this.save();
} catch (IOException exception) {
BlacksmithVisuals.getInstance().getLogger().log(Level.SEVERE, "Unable to save NPC data. Error was: " +
exception);
}
}
/**
* Saves this data manager's data to disk
*
* @throws IOException <p>If unable to write the configuration file</p>
*/
public void save() throws IOException {
if (!configurationFile.exists() && !configurationFile.createNewFile()) {
throw new FileNotFoundException("data.yml could not be found or created. Please create it manually.");
}
FileConfiguration configuration = new YamlConfiguration();
for (Map.Entry<UUID, NPCData> entry : npcDataMap.entrySet()) {
ConfigurationSection npcSection = configuration.createSection(entry.getKey().toString());
npcSection.set("workingPositionNetherite", entry.getValue().positions().get(NPCPosition.WORKING_NETHERITE));
npcSection.set("workingPositionCrafting", entry.getValue().positions().get(NPCPosition.WORKING_CRAFTING));
npcSection.set("workingPositionRepairable", entry.getValue().positions().get(NPCPosition.WORKING_REPAIRABLE));
npcSection.set("idlePosition", entry.getValue().positions().get(NPCPosition.IDLE));
}
configuration.save(configurationFile);
}
/**
* Loads a data manager from disk
*
* @return <p>The loaded data manager</p>
* @throws IOException <p>If unable to load the configuration file</p>
*/
@NotNull
public static NPCDataManager load() throws IOException {
if (!configurationFile.exists() && !configurationFile.createNewFile()) {
throw new FileNotFoundException("data.yml could not be found or created. Please create it manually.");
}
Map<UUID, NPCData> npcDataMap = new HashMap<>();
FileConfiguration configuration = YamlConfiguration.loadConfiguration(configurationFile);
for (String configurationSectionKey : configuration.getKeys(false)) {
ConfigurationSection section = configuration.getConfigurationSection(configurationSectionKey);
if (section == null) {
continue;
}
Map<NPCPosition, Location> locationMap = new HashMap<>();
locationMap.put(NPCPosition.WORKING_NETHERITE, section.getLocation("workingPositionNetherite"));
locationMap.put(NPCPosition.WORKING_CRAFTING, section.getLocation("workingPositionCrafting"));
locationMap.put(NPCPosition.WORKING_REPAIRABLE, section.getLocation("workingPositionRepairable"));
locationMap.put(NPCPosition.IDLE, section.getLocation("idlePosition"));
npcDataMap.put(UUID.fromString(configurationSectionKey), new NPCData(locationMap));
}
return new NPCDataManager(npcDataMap);
}
}

View File

@ -0,0 +1,82 @@
package net.knarcraft.blacksmithvisuals.property;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A definable NPC position
*/
public enum NPCPosition {
/**
* The position the NPC should be in while idle
*/
IDLE("idle"),
/**
* The position the NPC should be in while undoing netherite or armor trims
*/
WORKING_NETHERITE("netherite-workstation"),
/**
* The position the NPC should be in while reforging armor, weapons or tools
*/
WORKING_REPAIRABLE("repairable-workstation"),
/**
* The position the NPC should be in while un-crafting items
*/
WORKING_CRAFTING("crafting-workstation"),
;
private final String positionName;
NPCPosition(@NotNull String positionName) {
this.positionName = positionName;
}
/**
* Gets the user-friendly name for this position
*
* @return <p>The position name</p>
*/
@NotNull
public String getPositionName() {
return this.positionName;
}
/**
* Gets the NPC position the work station's material
*
* @param material <p>The material of the workstation</p>
* @return <p>The corresponding NPC position</p>
*/
@Nullable
public static NPCPosition getFromMaterial(@NotNull Material material) {
return switch (material) {
case CRAFTING_TABLE -> NPCPosition.WORKING_CRAFTING;
case ANVIL -> NPCPosition.WORKING_REPAIRABLE;
case SMITHING_TABLE -> NPCPosition.WORKING_NETHERITE;
default -> null;
};
}
/**
* Gets an NPC position from the given user input
*
* @param input <p>The input to get an NPC position from</p>
* @return <p>The NPC position, or null if not recognized</p>
*/
@Nullable
public static NPCPosition fromInput(@NotNull String input) {
String cleaned = input.toLowerCase().replace("_", "-");
for (NPCPosition position : NPCPosition.values()) {
if (position.getPositionName().equalsIgnoreCase(cleaned)) {
return position;
}
}
return null;
}
}

View File

@ -0,0 +1,61 @@
package net.knarcraft.blacksmithvisuals.property;
import org.jetbrains.annotations.NotNull;
/**
* An identifier for the available blacksmith/scrapper sounds
*/
public enum SoundIdentifier {
/**
* The sound played while a blacksmith is working
*/
REFORGING_WORKING("blacksmith.sounds.reforgeWorking"),
/**
* The sound played when a blacksmith finishes successfully
*/
REFORGING_SUCCESS("blacksmith.sounds.reforgeSuccess"),
/**
* The sound played when a blacksmith finishes with a failure
*/
REFORGING_FAILURE("blacksmith.sounds.reforgeFailure"),
/**
* The sound played while a scrapper is working
*/
SALVAGING_WORKING("scrapper.sounds.salvageWorking"),
/**
* The sound played when a scrapper finishes successfully
*/
SALVAGING_SUCCESS("scrapper.sounds.salvageSuccess"),
/**
* The sound played when a scrapper finishes with a failure
*/
SALVAGING_FAILURE("scrapper.sounds.salvageFailure"),
;
private final String configNode;
/**
* Instantiates a new sound identifier
*
* @param configNode <p>The sound's configuration node</p>
*/
SoundIdentifier(@NotNull String configNode) {
this.configNode = configNode;
}
/**
* Gets the configuration node corresponding to the sound
*
* @return <p>The configuration node</p>
*/
public String getConfigNode() {
return this.configNode;
}
}

View File

@ -0,0 +1,98 @@
blacksmith:
animation:
# Whether to simulate hitting an anvil or similar by animating the blacksmith's off-hand
animateOffhand: true
# The delay between each potential arm move in ticks
animationDelay: 5
# The probability (percentage) of an arm swing happening once the animation delay is reached
animationChance: 20
# Sounds that play on specific actions or events
sounds:
# The sound played when reforging finishes, if successful
reforgeSuccess:
# If disabled, this sound will never play
enabled: true
# The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html)
sound: BLOCK_ANVIL_USE
# The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html)
soundCategory: AMBIENT
# The pitch to play at, between 0 and 2. 1 is default.
pitch: 1
# The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range.
volume: 1
# The sound played when reforging finishes, if not successful.
reforgeFailure:
# If disabled, this sound will never play.
enabled: true
# The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html).
sound: ENTITY_VILLAGER_NO
# The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html)
soundCategory: AMBIENT
# The pitch to play at, between 0 and 2. 1 is default.
pitch: 1
# The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range.
volume: 1
# The sound played when the NPC's arm moves.
reforgeWorking:
# If disabled, this sound will never play.
enabled: true
# The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html).
sound: ITEM_ARMOR_EQUIP_NETHERITE
# The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html)
soundCategory: AMBIENT
# The pitch to play at, between 0 and 2. 1 is default.
pitch: 1
# The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range.
volume: 1
# The offset (delay), in ticks (negative or positive), before the sound should play.
# This can be used to better synchronize the sound to the arm movement.
offset: 0
scrapper:
animation:
# Whether to simulate hitting an anvil or similar by animating the scrapper's off-hand
animateOffhand: true
# The delay between each potential arm move in ticks
animationDelay: 5
# The probability (percentage) of an arm swing happening once the animation delay is reached
animationChance: 20
# Sounds that play on specific actions or events.
sounds:
# The sound played when salvaging finishes, if successful.
salvageSuccess:
# If disabled, this sound will never play.
enabled: true
# The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html).
sound: BLOCK_ANVIL_USE
# The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html)
soundCategory: AMBIENT
# The pitch to play at, between 0 and 2. 1 is default.
pitch: 1
# The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range.
volume: 1
# The sound played when salvaging finishes, if not successful.
salvageFailure:
# If disabled, this sound will never play.
enabled: true
# The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html).
sound: ENTITY_VILLAGER_NO
# The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html)
soundCategory: AMBIENT
# The pitch to play at, between 0 and 2. 1 is default.
pitch: 1
# The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range.
volume: 1
# The sound played when the NPC's arm moves.
salvageWorking:
# If disabled, this sound will never play.
enabled: true
# The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html).
sound: ITEM_ARMOR_EQUIP_NETHERITE
# The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html)
soundCategory: AMBIENT
# The pitch to play at, between 0 and 2. 1 is default.
pitch: 1
# The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range.
volume: 1
# The offset (delay), in ticks (negative or positive), before the sound should play.
# This can be used to better synchronize the sound to the arm movement.
offset: 0

View File

@ -2,4 +2,35 @@ name: BlacksmithVisuals
version: '${project.version}' version: '${project.version}'
main: net.knarcraft.blacksmithvisuals.BlacksmithVisuals main: net.knarcraft.blacksmithvisuals.BlacksmithVisuals
api-version: 1.20 api-version: 1.20
depend: [ ProtocolLib ] depend: [ ProtocolLib, Blacksmith, Citizens ]
prefix: "Blacksmith Visuals"
description: "And add-on plugin to the blacksmith plugin that adds visual animation and configurable sounds."
website: "https://git.knarcraft.net/KnarCraft/BlacksmithVisuals"
commands:
reload:
permission: blacksmithvisuals.reload
usage: /<command>
description: Used to reload BlacksmithVisuals
setNPCPosition:
permission: blacksmithvisuals.setposition
usage: /<command> <position type>
description: |
Used to set a blacksmith or scrapper NPC's positions
/setNPCPosition idle
/setNPCPosition netherite-workstation
/setNPCPosition reforging-workstation
/setNPCPosition crafting-workstation
permissions:
blacksmithvisuals.*:
description: Gives all permissions
default: op
children:
- blacksmithvisuals.reload
- blacksmithvisuals.setposition
blacksmithvisuals.reload:
description: Allows reloading the plugin
default: false
blacksmithvisuals.setposition:
description: Allows use of the /setIdlePosition and /setWorkingPosition commands
default: false