diff --git a/build.gradle.kts b/build.gradle.kts index 8c5f26ac..0505f5e3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +14,7 @@ val paperVersion: List = (property("gameVersions") as String) plugins { - `java-library` - id("org.jetbrains.kotlin.jvm") version "1.9.10" + id("org.jetbrains.kotlin.jvm") version "2.1.0" id("com.github.johnrengelman.shadow") version "8.1.1" // For ingametesting //id("io.papermc.paperweight.userdev") version "1.5.10" @@ -76,25 +75,23 @@ version = "2.0.5-beta" // x-release-please-version description = "OldCombatMechanics" -java { - toolchain { - // We can build with Java 17 but still support MC >=1.9 - // This is because MC >=1.9 server can be run with higher Java versions - languageVersion.set(JavaLanguageVersion.of(17)) - } -} - sourceSets { main { - java { + kotlin { exclude("kernitus/plugin/OldCombatMechanics/tester/**") } } } +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + tasks.withType { kotlinOptions { - jvmTarget = "17" // Ensure compatibility with Java 17 + jvmTarget = "17" } } @@ -106,10 +103,6 @@ tasks.named("processResources") { } } -tasks.withType { - options.encoding = "UTF-8" -} - tasks.named("shadowJar") { dependsOn("jar") archiveFileName.set("${project.name}.jar") diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/ModuleLoader.java b/src/main/java/kernitus/plugin/OldCombatMechanics/ModuleLoader.java deleted file mode 100644 index 7f8406f8..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/ModuleLoader.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics; - -import kernitus.plugin.OldCombatMechanics.module.OCMModule; -import kernitus.plugin.OldCombatMechanics.utilities.EventRegistry; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; - -import java.util.ArrayList; -import java.util.List; - -public class ModuleLoader { - - private static EventRegistry eventRegistry; - private static final List modules = new ArrayList<>(); - - public static void initialise(OCMMain plugin) { - ModuleLoader.eventRegistry = new EventRegistry(plugin); - } - - public static void toggleModules() { - modules.forEach(module -> setState(module, module.isEnabled())); - } - - private static void setState(OCMModule module, boolean state) { - if (state) { - if (eventRegistry.registerListener(module)) { - Messenger.debug("Enabled " + module.getClass().getSimpleName()); - } - } else { - if (eventRegistry.unregisterListener(module)) { - Messenger.debug("Disabled " + module.getClass().getSimpleName()); - } - } - } - - public static void addModule(OCMModule module) { - modules.add(module); - } - - public static List getModules() { - return modules; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/OCMConfigHandler.java b/src/main/java/kernitus/plugin/OldCombatMechanics/OCMConfigHandler.java deleted file mode 100644 index d9563aff..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/OCMConfigHandler.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics; - -import kernitus.plugin.OldCombatMechanics.utilities.Config; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Objects; - -public class OCMConfigHandler { - private final String CONFIG_NAME = "config.yml"; - private final OCMMain plugin; - - public OCMConfigHandler(OCMMain instance) { - this.plugin = instance; - } - - public void upgradeConfig() { - // Remove old backup file if present - final File backup = getFile("config-backup.yml"); - if (backup.exists()) backup.delete(); - - // Keeping YAML comments not available in lower versions - if (Reflector.versionIsNewerOrEqualTo(1, 18, 1) || - Config.getConfig().getBoolean("force-below-1-18-1-config-upgrade", false) - ) { - plugin.getLogger().warning("Config version does not match, upgrading old config"); - // Load values from old config - final YamlConfiguration oldConfig = getConfig(CONFIG_NAME); - final YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration( - new InputStreamReader(Objects.requireNonNull(plugin.getResource(CONFIG_NAME)))); - - // Copies value from old config if present in new config - for (String key : defaultConfig.getKeys(true)) { - if (key.equals("config-version") || !oldConfig.contains(key)) continue; - - if (defaultConfig.isConfigurationSection(key)) continue; // Only get leaf keys - - defaultConfig.set(key, oldConfig.get(key)); - } - try { - // Overwrites old file if needed - defaultConfig.save(getFile(CONFIG_NAME)); - plugin.getLogger().info("Config has been updated"); - } catch (IOException e) { - plugin.getLogger().severe("Failed to upgrade config"); - } - } else { - plugin.getLogger().warning("Config version does not match, backing up old config and creating a new one"); - // Change name of old config - final File configFile = getFile(CONFIG_NAME); - configFile.renameTo(backup); - } - - // Save new version if none is present - setupConfigIfNotPresent(); - } - - /** - * Generates new config.yml file, if not present. - */ - public void setupConfigIfNotPresent() { - if (!doesConfigExist()) { - plugin.saveDefaultConfig(); - plugin.getLogger().info("Config file generated"); - } - } - - public YamlConfiguration getConfig(String fileName) { - return YamlConfiguration.loadConfiguration(getFile(fileName)); - } - - public File getFile(String fileName) { - return new File(plugin.getDataFolder(), fileName.replace('/', File.separatorChar)); - } - - public boolean doesConfigExist() { - return getFile(CONFIG_NAME).exists(); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/OCMMain.java b/src/main/java/kernitus/plugin/OldCombatMechanics/OCMMain.java deleted file mode 100644 index e73ad62d..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/OCMMain.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics; - -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.ProtocolManager; -import kernitus.plugin.OldCombatMechanics.commands.OCMCommandCompleter; -import kernitus.plugin.OldCombatMechanics.commands.OCMCommandHandler; -import kernitus.plugin.OldCombatMechanics.hooks.PlaceholderAPIHook; -import kernitus.plugin.OldCombatMechanics.hooks.api.Hook; -import kernitus.plugin.OldCombatMechanics.module.*; -import kernitus.plugin.OldCombatMechanics.updater.ModuleUpdateChecker; -import kernitus.plugin.OldCombatMechanics.utilities.Config; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import kernitus.plugin.OldCombatMechanics.utilities.damage.AttackCooldownTracker; -import kernitus.plugin.OldCombatMechanics.utilities.damage.EntityDamageByEntityListener; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import kernitus.plugin.OldCombatMechanics.utilities.storage.ModesetListener; -import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage; -import org.bstats.bukkit.Metrics; -import org.bstats.charts.SimpleBarChart; -import org.bstats.charts.SimplePie; -import org.bukkit.Bukkit; -import org.bukkit.entity.HumanEntity; -import org.bukkit.event.EventException; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.PluginDescriptionFile; -import org.bukkit.plugin.RegisteredListener; -import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -public class OCMMain extends JavaPlugin { - - private static OCMMain INSTANCE; - private final Logger logger = getLogger(); - private final OCMConfigHandler CH = new OCMConfigHandler(this); - private final List disableListeners = new ArrayList<>(); - private final List enableListeners = new ArrayList<>(); - private final List hooks = new ArrayList<>(); - private ProtocolManager protocolManager; - - public OCMMain() { - super(); - } - - @Override - public void onEnable() { - INSTANCE = this; - - // Setting up config.yml - CH.setupConfigIfNotPresent(); - - // Initialise persistent player storage - PlayerStorage.initialise(this); - - // Initialise ModuleLoader utility - ModuleLoader.initialise(this); - - // Initialise Config utility - Config.initialise(this); - - // Initialise the Messenger utility - Messenger.initialise(this); - - try { - if (getServer().getPluginManager().getPlugin("ProtocolLib") != null && - getServer().getPluginManager().getPlugin("ProtocolLib").isEnabled()) - protocolManager = ProtocolLibrary.getProtocolManager(); - } catch (Exception e) { - Messenger.warn("No ProtocolLib detected, some features might be disabled"); - } - - // Register all the modules - registerModules(); - - // Register all hooks for integrating with other plugins - registerHooks(); - - // Initialise all the hooks - hooks.forEach(hook -> hook.init(this)); - - // Set up the command handler - getCommand("OldCombatMechanics").setExecutor(new OCMCommandHandler(this)); - // Set up command tab completer - getCommand("OldCombatMechanics").setTabCompleter(new OCMCommandCompleter()); - - Config.reload(); - - // BStats Metrics - final Metrics metrics = new Metrics(this, 53); - - // Simple bar chart - metrics.addCustomChart( - new SimpleBarChart( - "enabled_modules", - () -> ModuleLoader.getModules().stream() - .filter(OCMModule::isEnabled) - .collect(Collectors.toMap(OCMModule::toString, module -> 1)) - ) - ); - - // Pie chart of enabled/disabled for each module - ModuleLoader.getModules().forEach(module -> metrics.addCustomChart( - new SimplePie(module.getModuleName() + "_pie", - () -> module.isEnabled() ? "enabled" : "disabled" - ))); - - enableListeners.forEach(Runnable::run); - - // Properly handle Plugman load/unload. - final List joinListeners = Arrays.stream(PlayerJoinEvent.getHandlerList().getRegisteredListeners()) - .filter(registeredListener -> registeredListener.getPlugin().equals(this)) - .collect(Collectors.toList()); - - Bukkit.getOnlinePlayers().forEach(player -> { - final PlayerJoinEvent event = new PlayerJoinEvent(player, ""); - - // Trick all the modules into thinking the player just joined in case the plugin was loaded with Plugman. - // This way attack speeds, item modifications, etc. will be applied immediately instead of after a re-log. - joinListeners.forEach(registeredListener -> { - try { - registeredListener.callEvent(event); - } catch (EventException e) { - e.printStackTrace(); - } - }); - }); - - // Logging to console the enabling of OCM - final PluginDescriptionFile pdfFile = this.getDescription(); - logger.info(pdfFile.getName() + " v" + pdfFile.getVersion() + " has been enabled"); - - if (Config.moduleEnabled("update-checker")) - Bukkit.getScheduler().runTaskLaterAsynchronously(this, - () -> new UpdateChecker(this).performUpdate(), 20L); - - metrics.addCustomChart(new SimplePie("auto_update_pie", - () -> Config.moduleSettingEnabled("update-checker", - "auto-update") ? "enabled" : "disabled")); - - } - - @Override - public void onDisable() { - final PluginDescriptionFile pdfFile = this.getDescription(); - - disableListeners.forEach(Runnable::run); - - // Properly handle Plugman load/unload. - final List quitListeners = Arrays.stream(PlayerQuitEvent.getHandlerList().getRegisteredListeners()) - .filter(registeredListener -> registeredListener.getPlugin().equals(this)) - .collect(Collectors.toList()); - - // Trick all the modules into thinking the player just quit in case the plugin was unloaded with Plugman. - // This way attack speeds, item modifications, etc. will be restored immediately instead of after a disconnect. - Bukkit.getOnlinePlayers().forEach(player -> { - final PlayerQuitEvent event = new PlayerQuitEvent(player, ""); - - quitListeners.forEach(registeredListener -> { - try { - registeredListener.callEvent(event); - } catch (EventException e) { - e.printStackTrace(); - } - }); - }); - - PlayerStorage.instantSave(); - - // Logging to console the disabling of OCM - logger.info(pdfFile.getName() + " v" + pdfFile.getVersion() + " has been disabled"); - } - - private void registerModules() { - // Update Checker (also a module, so we can use the dynamic registering/unregistering) - ModuleLoader.addModule(new ModuleUpdateChecker(this)); - - // Modeset listener, for when player joins or changes world - ModuleLoader.addModule(new ModesetListener(this)); - - // Module listeners - ModuleLoader.addModule(new ModuleAttackCooldown(this)); - - // If below 1.16, we need to keep track of player attack cooldown ourselves - if (Reflector.getMethod(HumanEntity.class, "getAttackCooldown", 0) == null) { - ModuleLoader.addModule(new AttackCooldownTracker(this)); - } - - //Listeners registered later with same priority are called later - - //These four listen to OCMEntityDamageByEntityEvent: - ModuleLoader.addModule(new ModuleOldToolDamage(this)); - ModuleLoader.addModule(new ModuleSwordSweep(this)); - ModuleLoader.addModule(new ModuleOldPotionEffects(this)); - ModuleLoader.addModule(new ModuleOldCriticalHits(this)); - - //Next block are all on LOWEST priority, so will be called in the following order: - // Damage order: base -> potion effects -> critical hit -> enchantments - // Defence order: overdamage -> blocking -> armour -> resistance -> armour enchs -> absorption - //EntityDamageByEntityListener calls OCMEntityDamageByEntityEvent, see modules above - // For everything from base to overdamage - ModuleLoader.addModule(new EntityDamageByEntityListener(this)); - // ModuleSwordBlocking to calculate blocking - ModuleLoader.addModule(new ModuleShieldDamageReduction(this)); - // OldArmourStrength for armour -> resistance -> armour enchs -> absorption - ModuleLoader.addModule(new ModuleOldArmourStrength(this)); - - ModuleLoader.addModule(new ModuleSwordBlocking(this)); - ModuleLoader.addModule(new ModuleOldArmourDurability(this)); - - ModuleLoader.addModule(new ModuleGoldenApple(this)); - ModuleLoader.addModule(new ModuleFishingKnockback(this)); - ModuleLoader.addModule(new ModulePlayerKnockback(this)); - ModuleLoader.addModule(new ModulePlayerRegen(this)); - - ModuleLoader.addModule(new ModuleDisableCrafting(this)); - ModuleLoader.addModule(new ModuleDisableOffHand(this)); - ModuleLoader.addModule(new ModuleOldBrewingStand(this)); - ModuleLoader.addModule(new ModuleDisableProjectileRandomness(this)); - ModuleLoader.addModule(new ModuleDisableBowBoost(this)); - ModuleLoader.addModule(new ModuleProjectileKnockback(this)); - ModuleLoader.addModule(new ModuleNoLapisEnchantments(this)); - ModuleLoader.addModule(new ModuleDisableEnderpearlCooldown(this)); - ModuleLoader.addModule(new ModuleChorusFruit(this)); - - ModuleLoader.addModule(new ModuleOldBurnDelay(this)); - ModuleLoader.addModule(new ModuleAttackFrequency(this)); - ModuleLoader.addModule(new ModuleFishingRodVelocity(this)); - - // These modules require ProtocolLib - if (protocolManager != null) { - ModuleLoader.addModule(new ModuleAttackSounds(this)); - ModuleLoader.addModule(new ModuleSwordSweepParticles(this)); - } else { - Messenger.warn("No ProtocolLib detected, attack-sounds and sword-sweep-particles modules will be disabled"); - } - } - - private void registerHooks() { - if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) - hooks.add(new PlaceholderAPIHook()); - } - - public void upgradeConfig() { - CH.upgradeConfig(); - } - - public boolean doesConfigExist() { - return CH.doesConfigExist(); - } - - /** - * Registers a runnable to run when the plugin gets disabled. - * - * @param action the {@link Runnable} to run when the plugin gets disabled - */ - public void addDisableListener(Runnable action) { - disableListeners.add(action); - } - - /** - * Registers a runnable to run when the plugin gets enabled. - * - * @param action the {@link Runnable} to run when the plugin gets enabled - */ - public void addEnableListener(Runnable action) { - enableListeners.add(action); - } - - /** - * Get the plugin's JAR file - * - * @return The File object corresponding to this plugin - */ - @NotNull - @Override - public File getFile() { - return super.getFile(); - } - - public static OCMMain getInstance() { - return INSTANCE; - } - - public static String getVersion() { - return INSTANCE.getDescription().getVersion(); - } - - public ProtocolManager getProtocolManager() { - return protocolManager; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/UpdateChecker.java b/src/main/java/kernitus/plugin/OldCombatMechanics/UpdateChecker.java deleted file mode 100644 index c6f71d00..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/UpdateChecker.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics; - -import kernitus.plugin.OldCombatMechanics.updater.SpigetUpdateChecker; -import kernitus.plugin.OldCombatMechanics.utilities.Config; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import org.bukkit.ChatColor; -import org.bukkit.entity.Player; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -public class UpdateChecker { - private final SpigetUpdateChecker updater; - private final boolean autoDownload; - private final OCMMain plugin; - - public UpdateChecker(OCMMain plugin) { - updater = new SpigetUpdateChecker(); - this.plugin = plugin; - // We don't really want to auto update if the config is not going to be upgraded automatically - autoDownload = Config.moduleSettingEnabled("update-checker", "auto-update") && - (Reflector.versionIsNewerOrEqualTo(1, 18, 1) || - Config.getConfig().getBoolean("force-below-1-18-1-config-upgrade", false) - ); - } - - - public void performUpdate() { - performUpdate(null); - } - - public void performUpdate(@Nullable Player player) { - if (player != null) - update(player::sendMessage); - else - update(Messenger::info); - } - - private void update(Consumer target) { - final List messages = new ArrayList<>(); - if (updater.isUpdateAvailable()) { - messages.add(ChatColor.BLUE + "An update for OldCombatMechanics to version " + updater.getLatestVersion() + " is available!"); - if (!autoDownload) { - messages.add(ChatColor.BLUE + "Click here to download it: " + ChatColor.GRAY + updater.getUpdateURL()); - } else { - messages.add(ChatColor.BLUE + "Downloading update: " + ChatColor.GRAY + updater.getUpdateURL()); - try { - if (updater.downloadLatestVersion(plugin.getServer().getUpdateFolderFile(), plugin.getFile().getName())) - messages.add(ChatColor.GREEN + "Update downloaded. Restart or reload server to enable new version."); - else throw new RuntimeException(); - } catch (Exception e) { - messages.add(ChatColor.RED + "Error occurred while downloading update! Check console for more details"); - e.printStackTrace(); - } - } - } - - messages.forEach(target); - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/hooks/PlaceholderAPIHook.java b/src/main/java/kernitus/plugin/OldCombatMechanics/hooks/PlaceholderAPIHook.java deleted file mode 100644 index 1df329c8..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/hooks/PlaceholderAPIHook.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.hooks; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.hooks.api.Hook; -import kernitus.plugin.OldCombatMechanics.module.ModuleDisableEnderpearlCooldown; -import kernitus.plugin.OldCombatMechanics.module.ModuleGoldenApple; -import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerData; -import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage; -import me.clip.placeholderapi.expansion.PlaceholderExpansion; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public class PlaceholderAPIHook implements Hook { - private PlaceholderExpansion expansion; - - @Override - public void init(OCMMain plugin) { - expansion = new PlaceholderExpansion() { - @Override - public boolean canRegister() { - return true; - } - - @Override - public boolean persist() { - return true; - } - - @Override - public @NotNull String getIdentifier() { - return "ocm"; - } - - @Override - public @NotNull String getAuthor() { - return String.join(", ", plugin.getDescription().getAuthors()); - } - - @Override - public @NotNull String getVersion() { - return plugin.getDescription().getVersion(); - } - - @Override - public String onPlaceholderRequest(Player player, @NotNull String identifier) { - if (player == null) return null; - - switch (identifier) { - case "modeset": - return getModeset(player); - case "gapple_cooldown": - return getGappleCooldown(player); - case "napple_cooldown": - return getNappleCooldown(player); - case "enderpearl_cooldown": - return getEnderpearlCooldown(player); - } - - return null; - } - - private String getGappleCooldown(Player player) { - final long seconds = ModuleGoldenApple.getInstance().getGappleCooldown(player.getUniqueId()); - return seconds > 0 ? String.valueOf(seconds) : "None"; - } - - private String getNappleCooldown(Player player) { - final long seconds = ModuleGoldenApple.getInstance().getNappleCooldown(player.getUniqueId()); - return seconds > 0 ? String.valueOf(seconds) : "None"; - } - - private String getEnderpearlCooldown(Player player) { - final long seconds = ModuleDisableEnderpearlCooldown.getInstance().getEnderpearlCooldown(player.getUniqueId()); - return seconds > 0 ? String.valueOf(seconds) : "None"; - } - - private String getModeset(Player player) { - final PlayerData playerData = PlayerStorage.getPlayerData(player.getUniqueId()); - String modeName = playerData.getModesetForWorld(player.getWorld().getUID()); - if (modeName == null || modeName.isEmpty()) modeName = "unknown"; - return modeName; - } - }; - - expansion.register(); - } - - @Override - public void deinit(OCMMain plugin) { - if (expansion != null) { - expansion.unregister(); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackCooldown.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackCooldown.java deleted file mode 100644 index 85a4e29f..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackCooldown.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage; -import org.bukkit.Bukkit; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeInstance; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; - -/** - * Disables the attack cooldown. - */ -public class ModuleAttackCooldown extends OCMModule { - - private final double NEW_ATTACK_SPEED = 4; - - public ModuleAttackCooldown(OCMMain plugin) { - super(plugin, "disable-attack-cooldown"); - } - - @Override - public void reload() { - Bukkit.getOnlinePlayers().forEach(this::adjustAttackSpeed); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerLogin(PlayerJoinEvent e) { - adjustAttackSpeed(e.getPlayer()); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onWorldChange(PlayerChangedWorldEvent e) { - adjustAttackSpeed(e.getPlayer()); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerQuit(PlayerQuitEvent e) { - setAttackSpeed(e.getPlayer(), NEW_ATTACK_SPEED); - } - - /** - * Adjusts the attack speed to the default or configured value, depending on whether the module is enabled. - * - * @param player the player to set the attack speed for - */ - private void adjustAttackSpeed(Player player) { - final double attackSpeed = isEnabled(player) - ? module().getDouble("generic-attack-speed") - : NEW_ATTACK_SPEED; - - setAttackSpeed(player, attackSpeed); - } - - @Override - public void onModesetChange(Player player) { - adjustAttackSpeed(player); - } - - /** - * Sets the attack speed to the given value. - * - * @param player the player to set it for - * @param attackSpeed the attack speed to set it to - */ - public void setAttackSpeed(Player player, double attackSpeed) { - final AttributeInstance attribute = player.getAttribute(Attribute.GENERIC_ATTACK_SPEED); - if (attribute == null) return; - - final double baseValue = attribute.getBaseValue(); - - final String modesetName = PlayerStorage.getPlayerData(player.getUniqueId()).getModesetForWorld(player.getWorld().getUID()); - debug(String.format("Setting attack speed to %.2f (was: %.2f) for %s in mode %s", attackSpeed, baseValue, player.getName(), modesetName)); - - if (baseValue != attackSpeed) { - debug(String.format("Setting attack speed to %.2f (was: %.2f)", attackSpeed, baseValue), player); - - attribute.setBaseValue(attackSpeed); - player.saveData(); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackFrequency.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackFrequency.java deleted file mode 100644 index ff145231..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackFrequency.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.event.entity.EntityTeleportEvent; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerRespawnEvent; - -public class ModuleAttackFrequency extends OCMModule { - - private static final int DEFAULT_DELAY = 20; - private static int playerDelay, mobDelay; - - public ModuleAttackFrequency(OCMMain plugin) { - super(plugin, "attack-frequency"); - reload(); - } - - @Override - public void reload() { - playerDelay = module().getInt("playerDelay"); - mobDelay = module().getInt("mobDelay"); - - Bukkit.getWorlds().forEach(world -> world.getLivingEntities().forEach(livingEntity -> { - if (livingEntity instanceof Player) - livingEntity.setMaximumNoDamageTicks(isEnabled((Player) livingEntity) ? playerDelay : DEFAULT_DELAY); - else - livingEntity.setMaximumNoDamageTicks(isEnabled(world) ? mobDelay : DEFAULT_DELAY); - })); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent e) { - final Player player = e.getPlayer(); - if (isEnabled(player)) setDelay(player, playerDelay); - } - - @EventHandler - public void onPlayerLogout(PlayerQuitEvent e) { - setDelay(e.getPlayer(), DEFAULT_DELAY); - } - - @EventHandler - public void onPlayerChangeWorld(PlayerChangedWorldEvent e) { - final Player player = e.getPlayer(); - setDelay(player, isEnabled(player) ? playerDelay : DEFAULT_DELAY); - } - - @EventHandler - public void onPlayerRespawn(PlayerRespawnEvent e) { - final Player player = e.getPlayer(); - setDelay(player, isEnabled(player) ? playerDelay : DEFAULT_DELAY); - } - - private void setDelay(Player player, int delay) { - player.setMaximumNoDamageTicks(delay); - debug("Set hit delay to " + delay, player); - } - - @EventHandler - public void onCreatureSpawn(CreatureSpawnEvent e) { - final LivingEntity livingEntity = e.getEntity(); - final World world = livingEntity.getWorld(); - if (isEnabled(world)) livingEntity.setMaximumNoDamageTicks(mobDelay); - } - - @EventHandler - public void onEntityTeleportEvent(EntityTeleportEvent e) { - // This event is only fired for non-player entities - final Entity entity = e.getEntity(); - if (!(entity instanceof LivingEntity)) return; - final LivingEntity livingEntity = (LivingEntity) entity; - - final World fromWorld = e.getFrom().getWorld(); - final Location toLocation = e.getTo(); - if(toLocation == null) return; - final World toWorld = toLocation.getWorld(); - if (fromWorld.getUID() != toWorld.getUID()) - livingEntity.setMaximumNoDamageTicks(isEnabled(toWorld) ? mobDelay : DEFAULT_DELAY); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackSounds.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackSounds.java deleted file mode 100644 index a66a63f8..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleAttackSounds.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import com.comphenix.protocol.PacketType; -import com.comphenix.protocol.ProtocolManager; -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.events.PacketEvent; -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import org.bukkit.Sound; -import org.bukkit.plugin.Plugin; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -/** - * A module to disable the new attack sounds. - */ -public class ModuleAttackSounds extends OCMModule { - - private final ProtocolManager protocolManager = plugin.getProtocolManager(); - private final SoundListener soundListener = new SoundListener(plugin); - private final Set blockedSounds = new HashSet<>(getBlockedSounds()); - - public ModuleAttackSounds(OCMMain plugin) { - super(plugin, "disable-attack-sounds"); - - reload(); - } - - @Override - public void reload() { - blockedSounds.clear(); - blockedSounds.addAll(getBlockedSounds()); - - if (isEnabled()) - protocolManager.addPacketListener(soundListener); - else - protocolManager.removePacketListener(soundListener); - } - - private Collection getBlockedSounds() { - return module().getStringList("blocked-sound-names"); - } - - /** - * Disables attack sounds. - */ - private class SoundListener extends PacketAdapter { - private boolean disabledDueToError; - - public SoundListener(Plugin plugin) { - super(plugin, PacketType.Play.Server.NAMED_SOUND_EFFECT); - } - - @Override - public void onPacketSending(PacketEvent packetEvent) { - if (disabledDueToError || !isEnabled(packetEvent.getPlayer())) return; - - try { - final PacketContainer packetContainer = packetEvent.getPacket(); - final Sound sound = packetContainer.getSoundEffects().read(0); - - //fix NullpointerException when sending a custom sound - if (sound == null) { - return; - } - - final String soundName = sound.toString(); // Works for both string and namespaced key - - if (blockedSounds.contains(soundName)) { - packetEvent.setCancelled(true); - debug("Blocked sound " + soundName, packetEvent.getPlayer()); - } - } catch (Exception | ExceptionInInitializerError e) { - disabledDueToError = true; - Messenger.warn( - e, - "Error detecting sound packets. Please report it along with the following exception " + - "on github." - ); - } - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleChorusFruit.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleChorusFruit.java deleted file mode 100644 index 5358da2d..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleChorusFruit.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.MathsHelper; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.PlayerItemConsumeEvent; -import org.bukkit.event.player.PlayerTeleportEvent; - -import java.util.concurrent.ThreadLocalRandom; - -/** - * A module to control chorus fruits. - */ -public class ModuleChorusFruit extends OCMModule { - - public ModuleChorusFruit(OCMMain plugin) { - super(plugin, "chorus-fruit"); - } - - @EventHandler - public void onEat(PlayerItemConsumeEvent e) { - if (e.getItem().getType() != Material.CHORUS_FRUIT) return; - final Player player = e.getPlayer(); - - if (!isEnabled(player)) return; - - if (module().getBoolean("prevent-eating")) { - e.setCancelled(true); - return; - } - - final int hungerValue = module().getInt("hunger-value"); - final double saturationValue = module().getDouble("saturation-value"); - final int previousFoodLevel = player.getFoodLevel(); - final float previousSaturation = player.getSaturation(); - - // Run it on the next tick to reset things while not cancelling the chorus fruit eat event - // This ensures the teleport event is fired and counts towards statistics - Bukkit.getScheduler().runTaskLater(plugin, () -> { - final int newFoodLevel = Math.min(hungerValue + previousFoodLevel, 20); - final float newSaturation = Math.min((float) (saturationValue + previousSaturation), newFoodLevel); - - player.setFoodLevel(newFoodLevel); - player.setSaturation(newSaturation); - - debug("Food level changed from: " + previousFoodLevel + " to " + player.getFoodLevel(), player); - }, 2L); - } - - @EventHandler - public void onTeleport(PlayerTeleportEvent e) { - if (e.getCause() != PlayerTeleportEvent.TeleportCause.CHORUS_FRUIT) return; - - final Player player = e.getPlayer(); - if (!isEnabled(player)) return; - - final double distance = getMaxTeleportationDistance(); - - if (distance == 8) { - debug("Using vanilla teleport implementation!", player); - return; - } - - if (distance <= 0) { - debug("Chorus teleportation is not allowed", player); - e.setCancelled(true); - return; - } - - // Not sure when this can occur, but it is marked as @Nullable - final Location toLocation = e.getTo(); - - if (toLocation == null) { - debug("Teleport target is null", player); - return; - } - - final int maxheight = toLocation.getWorld().getMaxHeight(); - - e.setTo(player.getLocation().add( - ThreadLocalRandom.current().nextDouble(-distance, distance), - MathsHelper.clamp(ThreadLocalRandom.current().nextDouble(-distance, distance), 0, maxheight - 1), - ThreadLocalRandom.current().nextDouble(-distance, distance) - )); - } - - - private double getMaxTeleportationDistance() { - return module().getDouble("max-teleportation-distance"); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableBowBoost.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableBowBoost.java deleted file mode 100644 index ce80be72..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableBowBoost.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bukkit.entity.Arrow; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.projectiles.ProjectileSource; - -/** - * Prevents players from propelling themselves forward by shooting themselves. - */ -public class ModuleDisableBowBoost extends OCMModule { - - public ModuleDisableBowBoost(OCMMain plugin) { - super(plugin, "disable-bow-boost"); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onProjectileHit(EntityDamageByEntityEvent e) { - if (!(e.getEntity() instanceof Player)) return; - - final Player player = (Player) e.getEntity(); - - if (!(e.getDamager() instanceof Arrow)) return; - - final Arrow arrow = (Arrow) e.getDamager(); - - final ProjectileSource shooter = arrow.getShooter(); - - if (shooter instanceof Player) { - final Player shootingPlayer = (Player) shooter; - if (player.getUniqueId().equals(shootingPlayer.getUniqueId())) { - if (!isEnabled(player)) return; - - e.setCancelled(true); - debug("We cancelled your bow boost", player); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableCrafting.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableCrafting.java deleted file mode 100644 index a28aa9e2..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableCrafting.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import org.bukkit.Material; -import org.bukkit.entity.HumanEntity; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.PrepareItemCraftEvent; -import org.bukkit.inventory.CraftingInventory; -import org.bukkit.inventory.ItemStack; - -import java.util.List; - -/** - * Makes the specified materials uncraftable. - */ -public class ModuleDisableCrafting extends OCMModule { - - private List denied; - private String message; - - public ModuleDisableCrafting(OCMMain plugin) { - super(plugin, "disable-crafting"); - reload(); - } - - @Override - public void reload() { - denied = ConfigUtils.loadMaterialList(module(), "denied"); - message = module().getBoolean("showMessage") ? module().getString("message") : null; - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPrepareItemCraft(PrepareItemCraftEvent e) { - final List viewers = e.getViewers(); - if (viewers.size() == 0) return; - - if (!isEnabled(viewers.get(0))) return; - - final CraftingInventory inv = e.getInventory(); - final ItemStack result = inv.getResult(); - - if (result != null && denied.contains(result.getType())) { - inv.setResult(null); - if (message != null) viewers.forEach(viewer -> Messenger.send(viewer, message)); - } - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableEnderpearlCooldown.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableEnderpearlCooldown.java deleted file mode 100644 index ee2e5723..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableEnderpearlCooldown.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import org.bukkit.GameMode; -import org.bukkit.Material; -import org.bukkit.entity.EnderPearl; -import org.bukkit.entity.Player; -import org.bukkit.entity.Projectile; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.ProjectileLaunchEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.projectiles.ProjectileSource; - -import java.util.*; - -/** - * Allows you to throw enderpearls as often as you like, not only after a cooldown. - */ -public class ModuleDisableEnderpearlCooldown extends OCMModule { - - /** - * Contains players that threw an ender pearl. As the handler calls launchProjectile, - * which also calls ProjectileLaunchEvent, we need to ignore that event call. - */ - private final Set ignoredPlayers = new HashSet<>(); - private Map lastLaunched; - private int cooldown; - private String message; - private static ModuleDisableEnderpearlCooldown INSTANCE; - - public ModuleDisableEnderpearlCooldown(OCMMain plugin) { - super(plugin, "disable-enderpearl-cooldown"); - INSTANCE = this; - reload(); - } - - public void reload() { - cooldown = module().getInt("cooldown"); - if (cooldown > 0) { - if (lastLaunched == null) lastLaunched = new WeakHashMap<>(); - } else lastLaunched = null; - - message = module().getBoolean("showMessage") ? module().getString("message") : null; - } - - public static ModuleDisableEnderpearlCooldown getInstance() { - return INSTANCE; - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerShoot(ProjectileLaunchEvent e) { - if (e.isCancelled()) return; // For compatibility with other plugins - - final Projectile projectile = e.getEntity(); - if (!(projectile instanceof EnderPearl)) return; - final ProjectileSource shooter = projectile.getShooter(); - - if (!(shooter instanceof Player)) return; - final Player player = (Player) shooter; - - if (!isEnabled(player)) return; - - final UUID uuid = player.getUniqueId(); - - if (ignoredPlayers.contains(uuid)) return; - - e.setCancelled(true); - - // Check if the cooldown has expired yet - if (lastLaunched != null) { - final long currentTime = System.currentTimeMillis() / 1000; - if (lastLaunched.containsKey(uuid)) { - final long elapsedSeconds = currentTime - lastLaunched.get(uuid); - if (elapsedSeconds < cooldown) { - if (message != null) Messenger.send(player, message, cooldown - elapsedSeconds); - return; - } - } - - lastLaunched.put(uuid, currentTime); - } - - // Make sure we ignore the event triggered by launchProjectile - ignoredPlayers.add(uuid); - final EnderPearl pearl = player.launchProjectile(EnderPearl.class); - ignoredPlayers.remove(uuid); - - pearl.setVelocity(player.getEyeLocation().getDirection().multiply(2)); - - if (player.getGameMode() == GameMode.CREATIVE) return; - - final ItemStack enderpearlItemStack; - final PlayerInventory playerInventory = player.getInventory(); - final ItemStack mainHand = playerInventory.getItemInMainHand(); - final ItemStack offHand = playerInventory.getItemInOffHand(); - - if (isEnderPearl(mainHand)) enderpearlItemStack = mainHand; - else if (isEnderPearl(offHand)) enderpearlItemStack = offHand; - else return; - - enderpearlItemStack.setAmount(enderpearlItemStack.getAmount() - 1); - } - - private boolean isEnderPearl(ItemStack itemStack) { - return itemStack != null && itemStack.getType() == Material.ENDER_PEARL; - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent e) { - if (lastLaunched != null) lastLaunched.remove(e.getPlayer().getUniqueId()); - } - - /** - * Get the remaining cooldown time for ender pearls for a given player. - * @param playerUUID The UUID of the player to check the cooldown for. - * @return The remaining cooldown time in seconds, or 0 if there is no cooldown or it has expired. - */ - public long getEnderpearlCooldown(UUID playerUUID) { - if (lastLaunched != null && lastLaunched.containsKey(playerUUID)) { - final long currentTime = System.currentTimeMillis() / 1000; // Current time in seconds - final long lastLaunchTime = lastLaunched.get(playerUUID); // Last launch time in seconds - final long elapsedSeconds = currentTime - lastLaunchTime; - final long cooldownRemaining = cooldown - elapsedSeconds; - return Math.max(cooldownRemaining, 0); // Return the remaining cooldown or 0 if it has expired - } - return 0; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableOffHand.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableOffHand.java deleted file mode 100644 index 6261eed6..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableOffHand.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import org.bukkit.Material; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerSwapHandItemsEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryView; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; - -import java.util.Collection; -import java.util.List; -import java.util.function.BiPredicate; - -/** - * Disables usage of the off-hand. - */ -public class ModuleDisableOffHand extends OCMModule { - - private static final int OFFHAND_SLOT = 40; - private List materials; - private String deniedMessage; - private BlockType blockType; - - public ModuleDisableOffHand(OCMMain plugin) { - super(plugin, "disable-offhand"); - reload(); - } - - @Override - public void reload() { - blockType = module().getBoolean("whitelist") ? BlockType.WHITELIST : BlockType.BLACKLIST; - materials = ConfigUtils.loadMaterialList(module(), "items"); - deniedMessage = module().getString("denied-message"); - } - - private void sendDeniedMessage(CommandSender sender) { - if (!deniedMessage.trim().isEmpty()) - Messenger.send(sender, deniedMessage); - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onSwapHandItems(PlayerSwapHandItemsEvent e) { - final Player player = e.getPlayer(); - if (isEnabled(player) && isItemBlocked(e.getOffHandItem())) { - e.setCancelled(true); - sendDeniedMessage(player); - } - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onInventoryClick(InventoryClickEvent e) { - final HumanEntity player = e.getWhoClicked(); - if (!isEnabled(player)) return; - final ClickType clickType = e.getClick(); - - try { - if (clickType == ClickType.SWAP_OFFHAND) { - e.setResult(Event.Result.DENY); - sendDeniedMessage(player); - return; - } - } catch (NoSuchFieldError ignored) { - } // For versions below 1.16 - - final Inventory clickedInventory = e.getClickedInventory(); - if (clickedInventory == null) return; - final InventoryType inventoryType = clickedInventory.getType(); - // Source inventory must be PLAYER - if(inventoryType != InventoryType.PLAYER) return; - - final InventoryView view = e.getView(); - // If neither of the inventories is CRAFTING, player cannot be moving stuff to the offhand - if (view.getBottomInventory().getType() != InventoryType.CRAFTING && - view.getTopInventory().getType() != InventoryType.CRAFTING) - return; - - // Prevent shift-clicking a shield into the offhand item slot - final ItemStack currentItem = e.getCurrentItem(); - if (currentItem != null - && currentItem.getType() == Material.SHIELD - && isItemBlocked(currentItem) - && e.getSlot() != OFFHAND_SLOT - && e.isShiftClick()) { - e.setResult(Event.Result.DENY); - sendDeniedMessage(player); - } - - if (e.getSlot() == OFFHAND_SLOT && - // Let allowed items be placed into offhand slot with number keys (hotbar swap) - ((clickType == ClickType.NUMBER_KEY && isItemBlocked(clickedInventory.getItem(e.getHotbarButton()))) - || isItemBlocked(e.getCursor())) // Deny placing not allowed items into offhand slot - ) { - e.setResult(Event.Result.DENY); - sendDeniedMessage(player); - } - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onInventoryDrag(InventoryDragEvent e) { - final HumanEntity player = e.getWhoClicked(); - if (!isEnabled(player) - || e.getInventory().getType() != InventoryType.CRAFTING - || !e.getInventorySlots().contains(OFFHAND_SLOT)) return; - - if (isItemBlocked(e.getOldCursor())) { - e.setResult(Event.Result.DENY); - sendDeniedMessage(player); - } - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onWorldChange(PlayerChangedWorldEvent e) { - onModesetChange(e.getPlayer()); - } - - @Override - public void onModesetChange(Player player) { - final PlayerInventory inventory = player.getInventory(); - final ItemStack offHandItem = inventory.getItemInOffHand(); - - if (isItemBlocked(offHandItem)) { - sendDeniedMessage(player); - inventory.setItemInOffHand(new ItemStack(Material.AIR)); - if (!inventory.addItem(offHandItem).isEmpty()) - player.getWorld().dropItemNaturally(player.getLocation(), offHandItem); - } - } - - private boolean isItemBlocked(ItemStack item) { - if (item == null || item.getType() == Material.AIR) { - return false; - } - - return !blockType.isAllowed(materials, item.getType()); - } - - private enum BlockType { - WHITELIST(Collection::contains), - BLACKLIST(not(Collection::contains)); - - private final BiPredicate, Material> filter; - - BlockType(BiPredicate, Material> filter) { - this.filter = filter; - } - - /** - * Checks whether the given material is allowed. - * - * @param list the list to use for checking - * @param toCheck the material to check - * @return true if the item is allowed, based on the list and the current mode - */ - boolean isAllowed(Collection list, Material toCheck) { - return filter.test(list, toCheck); - } - } - - private static BiPredicate not(BiPredicate predicate) { - return predicate.negate(); - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableProjectileRandomness.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableProjectileRandomness.java deleted file mode 100644 index 738dc3b0..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleDisableProjectileRandomness.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser; -import org.bukkit.Material; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.entity.Projectile; -import org.bukkit.event.EventHandler; -import org.bukkit.event.entity.ProjectileLaunchEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.projectiles.ProjectileSource; -import org.bukkit.util.Vector; - -import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; - -/** - * Prevents the noise introduced when shooting with a bow to make arrows go straight. - */ -public class ModuleDisableProjectileRandomness extends OCMModule { - - private double EPSILON; - private List projectileTypes; - - // Method was added in 1.14.0 - private static final SpigotFunctionChooser rotateAroundY = SpigotFunctionChooser.apiCompatCall( - (vector, angle) -> vector.rotateAroundY(angle), - (vector, angle) -> { - double angleCos = Math.cos(angle); - double angleSin = Math.sin(angle); - - double x = angleCos * vector.getX() + angleSin * vector.getZ(); - double z = -angleSin * vector.getX() + angleCos * vector.getZ(); - return vector.setX(x).setZ(z); - } - ); - - public ModuleDisableProjectileRandomness(OCMMain plugin) { - super(plugin, "disable-projectile-randomness"); - reload(); - } - - @Override - public void reload() { - EPSILON = module().getDouble("epsilon"); - projectileTypes = module() - .getStringList("projectile-types").stream() - .map(str -> str.toUpperCase(Locale.ROOT)) - .map(EntityType::valueOf) - .collect(Collectors.toList()); - } - - @EventHandler - public void onProjectileLaunch(ProjectileLaunchEvent e) { - final Projectile projectile = e.getEntity(); - final ProjectileSource shooter = projectile.getShooter(); - - if (!projectileTypes.contains(e.getEntityType())) return; - - if (!(shooter instanceof Player)) return; - final Player player = (Player) shooter; - if (!isEnabled(player)) return; - - debug("Making projectile go straight", player); - - final Vector playerDirection = player.getLocation().getDirection().normalize(); - final Vector projectileDirection = projectile.getVelocity(); - - // Keep original speed - final double originalMagnitude = projectileDirection.length(); - projectileDirection.normalize(); - - final ItemStack item = player.getInventory().getItemInMainHand(); - - // If the projectile is not going straight (e.g. multishot arrows) - if (item.getType() == Material.CROSSBOW && item.getEnchantmentLevel(Enchantment.MULTISHOT) > 0) { - if (fuzzyVectorEquals(projectileDirection, rotateAroundY.apply(playerDirection.clone(), 0.17))) { - debug("10° Offset", player); // 10 degrees is 0.17 radians - rotateAroundY.apply(playerDirection, 0.17); - } else if (fuzzyVectorEquals(projectileDirection, rotateAroundY.apply(playerDirection.clone(), -0.17))) { - debug("-10° Offset", player); - rotateAroundY.apply(playerDirection, -0.17); - } - } - - playerDirection.multiply(originalMagnitude); - projectile.setVelocity(playerDirection); - } - - private boolean fuzzyVectorEquals(Vector a, Vector b) { - return Math.abs(a.getX() - b.getX()) < EPSILON && - Math.abs(a.getZ() - b.getZ()) < EPSILON; - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleFishingKnockback.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleFishingKnockback.java deleted file mode 100644 index 25cd02c3..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleFishingKnockback.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.*; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.ProjectileHitEvent; -import org.bukkit.event.player.PlayerFishEvent; -import org.bukkit.util.Vector; - -/** - * Brings back the old fishing-rod knockback. - */ -public class ModuleFishingKnockback extends OCMModule { - - private final SpigotFunctionChooser getHookFunction; - private final SpigotFunctionChooser getHitEntityFunction; - private boolean knockbackNonPlayerEntities; - - public ModuleFishingKnockback(OCMMain plugin) { - super(plugin, "old-fishing-knockback"); - - reload(); - - getHookFunction = SpigotFunctionChooser.apiCompatReflectionCall((e, params) -> e.getHook(), - PlayerFishEvent.class, "getHook"); - getHitEntityFunction = SpigotFunctionChooser.apiCompatCall((e, params) -> e.getHitEntity(), (e, params) -> { - final Entity hookEntity = e.getEntity(); - final World world = hookEntity.getWorld(); - return world.getNearbyEntities(hookEntity.getLocation(), 0.25, 0.25, 0.25).stream() - .filter(entity -> !knockbackNonPlayerEntities && entity instanceof Player) - .findFirst() - .orElse(null); - }); - } - - @Override - public void reload() { - knockbackNonPlayerEntities = isSettingEnabled("knockbackNonPlayerEntities"); - } - - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - public void onRodLand(ProjectileHitEvent event) { - final Entity hookEntity = event.getEntity(); - final World world = hookEntity.getWorld(); - - // FISHING_HOOK -> FISHING_BOBBER in >=1.20.5 - EntityType fishingBobberType; - try { - fishingBobberType = EntityType.FISHING_BOBBER; - } catch (NoSuchFieldError e) { - fishingBobberType = EntityType.valueOf("FISHING_HOOK"); - } - if (event.getEntityType() != fishingBobberType) return; - - final FishHook hook = (FishHook) hookEntity; - - if (!(hook.getShooter() instanceof Player rodder)) return; - if (!isEnabled(rodder)) return; - - final Entity hitEntity = getHitEntityFunction.apply(event); - - if (hitEntity == null) return; // If no entity was hit - if (!(hitEntity instanceof LivingEntity livingEntity)) return; - if (!knockbackNonPlayerEntities && !(hitEntity instanceof Player)) return; - - // Do not move Citizens NPCs - // See https://wiki.citizensnpcs.co/API#Checking_if_an_entity_is_a_Citizens_NPC - if (hitEntity.hasMetadata("NPC")) return; - - - if (!knockbackNonPlayerEntities) { - final Player player = (Player) hitEntity; - - debug("You were hit by a fishing rod!", player); - - if (player.equals(rodder)) return; - - if (player.getGameMode() == GameMode.CREATIVE) return; - } - - // Check if cooldown time has elapsed - if (livingEntity.getNoDamageTicks() > livingEntity.getMaximumNoDamageTicks() / 2f) return; - - double damage = module().getDouble("damage"); - if (damage < 0) damage = 0.0001; - - livingEntity.damage(damage, rodder); - livingEntity.setVelocity(calculateKnockbackVelocity(livingEntity.getVelocity(), livingEntity.getLocation(), hook.getLocation())); - } - - private Vector calculateKnockbackVelocity(Vector currentVelocity, Location player, Location hook) { - double xDistance = hook.getX() - player.getX(); - double zDistance = hook.getZ() - player.getZ(); - - // ensure distance is not zero and randomise in that case (I guess?) - while (xDistance * xDistance + zDistance * zDistance < 0.0001) { - xDistance = (Math.random() - Math.random()) * 0.01D; - zDistance = (Math.random() - Math.random()) * 0.01D; - } - - final double distance = Math.sqrt(xDistance * xDistance + zDistance * zDistance); - - double y = currentVelocity.getY() / 2; - double x = currentVelocity.getX() / 2; - double z = currentVelocity.getZ() / 2; - - // Normalise distance to have similar knockback, no matter the distance - x -= xDistance / distance * 0.4; - - // slow the fall or throw upwards - y += 0.4; - - // Normalise distance to have similar knockback, no matter the distance - z -= zDistance / distance * 0.4; - - // do not shoot too high up - if (y >= 0.4) - y = 0.4; - - return new Vector(x, y, z); - } - - /** - * This is to cancel dragging the entity closer when you reel in - */ - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - private void onReelIn(PlayerFishEvent e) { - if (e.getState() != PlayerFishEvent.State.CAUGHT_ENTITY) return; - - final String cancelDraggingIn = module().getString("cancelDraggingIn", "players"); - final boolean isPlayer = e.getCaught() instanceof HumanEntity; - if ((cancelDraggingIn.equals("players") && isPlayer) || - cancelDraggingIn.equals("mobs") && !isPlayer || - cancelDraggingIn.equals("all")) { - getHookFunction.apply(e).remove(); // Remove the bobber and don't do anything else - e.setCancelled(true); - } - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleFishingRodVelocity.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleFishingRodVelocity.java deleted file mode 100644 index 839dbad4..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleFishingRodVelocity.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.entity.FishHook; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.PlayerFishEvent; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.util.Vector; - -import java.util.Random; - -/** - * This module reverts fishing rod gravity and velocity back to 1.8 behaviour - *

- * Fishing rod gravity in 1.14+ is 0.03 while in 1.8 it is 0.04 - * Launch velocity in 1.9+ is also different from the 1.8 formula - */ -public class ModuleFishingRodVelocity extends OCMModule { - - private Random random; - private boolean hasDifferentGravity; - // In 1.12- getHook() returns a Fish which extends FishHook - private final SpigotFunctionChooser getHook = SpigotFunctionChooser.apiCompatReflectionCall( - (e, params) -> e.getHook(), - PlayerFishEvent.class, "getHook" - ); - - public ModuleFishingRodVelocity(OCMMain plugin) { - super(plugin, "fishing-rod-velocity"); - reload(); - } - - @Override - public void reload() { - random = new Random(); - - // Versions 1.14+ have different gravity than previous versions - hasDifferentGravity = Reflector.versionIsNewerOrEqualTo(1, 14, 0); - } - - @EventHandler (ignoreCancelled = true) - public void onFishEvent(PlayerFishEvent event) { - final FishHook fishHook = getHook.apply(event); - final Player player = event.getPlayer(); - - if (!isEnabled(player) || event.getState() != PlayerFishEvent.State.FISHING) return; - - final Location location = event.getPlayer().getLocation(); - final double playerYaw = location.getYaw(); - final double playerPitch = location.getPitch(); - - final float oldMaxVelocity = 0.4F; - double velocityX = -Math.sin(playerYaw / 180.0F * (float) Math.PI) * Math.cos(playerPitch / 180.0F * (float) Math.PI) * oldMaxVelocity; - double velocityZ = Math.cos(playerYaw / 180.0F * (float) Math.PI) * Math.cos(playerPitch / 180.0F * (float) Math.PI) * oldMaxVelocity; - double velocityY = -Math.sin(playerPitch / 180.0F * (float) Math.PI) * oldMaxVelocity; - - final double oldVelocityMultiplier = 1.5; - - final double vectorLength = (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY + velocityZ * velocityZ); - velocityX /= vectorLength; - velocityY /= vectorLength; - velocityZ /= vectorLength; - - velocityX += random.nextGaussian() * 0.007499999832361937D; - velocityY += random.nextGaussian() * 0.007499999832361937D; - velocityZ += random.nextGaussian() * 0.007499999832361937D; - - velocityX *= oldVelocityMultiplier; - velocityY *= oldVelocityMultiplier; - velocityZ *= oldVelocityMultiplier; - - fishHook.setVelocity(new Vector(velocityX, velocityY, velocityZ)); - - if (!hasDifferentGravity) return; - - // Adjust gravity on every tick unless it's in water - new BukkitRunnable() { - @Override - public void run() { - if (!fishHook.isValid() || fishHook.isOnGround()) cancel(); - - // We check both conditions as sometimes it's underwater but in seagrass, or when bobbing not underwater but the material is water - if (!fishHook.isInWater() && fishHook.getWorld().getBlockAt(fishHook.getLocation()).getType() != Material.WATER) { - final Vector fVelocity = fishHook.getVelocity(); - fVelocity.setY(fVelocity.getY() - 0.01); - fishHook.setVelocity(fVelocity); - } - } - }.runTaskTimer(plugin, 1, 1); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleGoldenApple.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleGoldenApple.java deleted file mode 100644 index 37284cf2..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleGoldenApple.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import com.google.common.collect.ImmutableSet; -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.PrepareItemCraftEvent; -import org.bukkit.event.player.PlayerItemConsumeEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ShapedRecipe; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; - -import static kernitus.plugin.OldCombatMechanics.versions.materials.MaterialRegistry.ENCHANTED_GOLDEN_APPLE; - -/** - * Customise the golden apple effects. - */ -public class ModuleGoldenApple extends OCMModule { - - // Default apple effects - // Gapple: absorption I, regen II - private static final Set gappleEffects = ImmutableSet.of(PotionEffectType.ABSORPTION, - PotionEffectType.REGENERATION); - // Napple: absorption IV, regen II, fire resistance I, resistance I - private static final Set nappleEffects = ImmutableSet.of(PotionEffectType.ABSORPTION, - PotionEffectType.REGENERATION, PotionEffectType.FIRE_RESISTANCE, PotionEffectTypeCompat.RESISTANCE.get()); - private List enchantedGoldenAppleEffects, goldenAppleEffects; - private ShapedRecipe enchantedAppleRecipe; - - private Map lastEaten; - private Cooldown cooldown; - - private String normalCooldownMessage, enchantedCooldownMessage; - private static ModuleGoldenApple INSTANCE; - - public ModuleGoldenApple(OCMMain plugin) { - super(plugin, "old-golden-apples"); - INSTANCE = this; - } - - @SuppressWarnings("deprecated") - @Override - public void reload() { - normalCooldownMessage = module().getString("cooldown.message-normal"); - enchantedCooldownMessage = module().getString("cooldown.message-enchanted"); - - cooldown = new Cooldown( - module().getLong("cooldown.normal"), - module().getLong("cooldown.enchanted"), - module().getBoolean("cooldown.is-shared") - ); - lastEaten = new WeakHashMap<>(); - - enchantedGoldenAppleEffects = getPotionEffects("enchanted-golden-apple-effects"); - goldenAppleEffects = getPotionEffects("golden-apple-effects"); - - try { - enchantedAppleRecipe = new ShapedRecipe( - new NamespacedKey(plugin, "MINECRAFT"), - ENCHANTED_GOLDEN_APPLE.newInstance() - ); - } catch (NoClassDefFoundError e) { - enchantedAppleRecipe = new ShapedRecipe(ENCHANTED_GOLDEN_APPLE.newInstance()); - } - enchantedAppleRecipe - .shape("ggg", "gag", "ggg") - .setIngredient('g', Material.GOLD_BLOCK) - .setIngredient('a', Material.APPLE); - - registerCrafting(); - } - - public static ModuleGoldenApple getInstance() { - return ModuleGoldenApple.INSTANCE; - } - - private void registerCrafting() { - if (isEnabled() && module().getBoolean("enchanted-golden-apple-crafting")) { - if (!Bukkit.getRecipesFor(ENCHANTED_GOLDEN_APPLE.newInstance()).isEmpty()) return; - Bukkit.addRecipe(enchantedAppleRecipe); - debug("Added napple recipe"); - } - } - - @EventHandler(priority = EventPriority.HIGH) - public void onPrepareItemCraft(PrepareItemCraftEvent e) { - final ItemStack item = e.getInventory().getResult(); - if (item == null) - return; // This should never ever ever ever run. If it does then you probably screwed something up. - - if (ENCHANTED_GOLDEN_APPLE.isSame(item)) { - final HumanEntity player = e.getView().getPlayer(); - - if (isSettingEnabled("no-conflict-mode")) return; - - if (!isEnabled(player) || !isSettingEnabled("enchanted-golden-apple-crafting")) - e.getInventory().setResult(null); - } - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onItemConsume(PlayerItemConsumeEvent e) { - final Player player = e.getPlayer(); - - if (!isEnabled(player)) return; - - final ItemStack originalItem = e.getItem(); - final Material consumedMaterial = originalItem.getType(); - - if (consumedMaterial != Material.GOLDEN_APPLE && - !ENCHANTED_GOLDEN_APPLE.isSame(originalItem)) return; - - final UUID uuid = player.getUniqueId(); - - // Check if the cooldown has expired yet - lastEaten.putIfAbsent(uuid, new LastEaten()); - - // If on cooldown send appropriate cooldown message - if (cooldown.isOnCooldown(originalItem, lastEaten.get(uuid))) { - final LastEaten le = lastEaten.get(uuid); - - final long baseCooldown; - Instant current; - final String message; - - if (consumedMaterial == Material.GOLDEN_APPLE) { - baseCooldown = cooldown.normal; - current = le.lastNormalEaten; - message = normalCooldownMessage; - } else { - baseCooldown = cooldown.enchanted; - current = le.lastEnchantedEaten; - message = enchantedCooldownMessage; - } - - final Optional newestEatTime = le.getNewestEatTime(); - if (cooldown.sharedCooldown && newestEatTime.isPresent()) - current = newestEatTime.get(); - - final long seconds = baseCooldown - (Instant.now().getEpochSecond() - current.getEpochSecond()); - - if (message != null && !message.isEmpty()) - Messenger.send(player, message.replaceAll("%seconds%", String.valueOf(seconds))); - - e.setCancelled(true); - return; - } - - lastEaten.get(uuid).setForItem(originalItem); - - if (!isSettingEnabled("old-potion-effects")) return; - - // Save player's current potion effects - final Collection previousPotionEffects = player.getActivePotionEffects(); - - final List newEffects = ENCHANTED_GOLDEN_APPLE.isSame(originalItem) ? - enchantedGoldenAppleEffects : goldenAppleEffects; - final Set defaultEffects = ENCHANTED_GOLDEN_APPLE.isSame(originalItem) ? - nappleEffects : gappleEffects; - - Bukkit.getScheduler().runTaskLater(plugin, () -> { - // Remove all potion effects the apple added - player.getActivePotionEffects().stream() - .map(PotionEffect::getType) - .filter(defaultEffects::contains) - .forEach(player::removePotionEffect); - // Add previous potion effects from before eating the apple - player.addPotionEffects(previousPotionEffects); - // Add new custom effects from eating the apple - applyEffects(player, newEffects); - }, 1L); - } - - - private void applyEffects(LivingEntity target, List newEffects) { - for (PotionEffect newEffect : newEffects) { - // Find the existing effect of the same type with the highest amplifier - final PotionEffect highestExistingEffect = target.getActivePotionEffects().stream() - .filter(e -> e.getType() == newEffect.getType()) - .max(Comparator.comparingInt(PotionEffect::getAmplifier)) - .orElse(null); - - if (highestExistingEffect != null) { - // If the new effect has a higher amplifier, apply it - if (newEffect.getAmplifier() > highestExistingEffect.getAmplifier()) { - target.addPotionEffect(newEffect); - } - // If the amplifiers are the same and the new effect has a longer duration, refresh the duration - else if (newEffect.getAmplifier() == highestExistingEffect.getAmplifier() && - newEffect.getDuration() > highestExistingEffect.getDuration()) { - target.addPotionEffect(newEffect); - } - // If the new effect has a lower amplifier or shorter/equal duration, do nothing - } else { - // If there is no existing effect of the same type, apply the new effect - target.addPotionEffect(newEffect); - } - } - } - - - private List getPotionEffects(String path) { - final List appleEffects = new ArrayList<>(); - - final ConfigurationSection sect = module().getConfigurationSection(path); - for (String key : sect.getKeys(false)) { - final int duration = sect.getInt(key + ".duration") * 20; // Convert seconds to ticks - final int amplifier = sect.getInt(key + ".amplifier"); - - final PotionEffectType type = PotionEffectTypeCompat.fromNewName(key); - Objects.requireNonNull(type, String.format("Invalid potion effect type '%s'!", key)); - - final PotionEffect fx = new PotionEffect(type, duration, amplifier); - appleEffects.add(fx); - } - return appleEffects; - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent e) { - final UUID uuid = e.getPlayer().getUniqueId(); - if (lastEaten != null) lastEaten.remove(uuid); - } - - /** - * Get player's current golden apple cooldown - * - * @param playerUUID The UUID of the player to check the cooldown for. - * @return The remaining cooldown time in seconds, or 0 if there is no cooldown, or it has expired. - */ - public long getGappleCooldown(UUID playerUUID) { - final LastEaten lastEatenInfo = lastEaten.get(playerUUID); - if (lastEatenInfo != null && lastEatenInfo.lastNormalEaten != null) { - long timeElapsedSinceEaten = Duration.between(lastEatenInfo.lastNormalEaten, Instant.now()).getSeconds(); - long cooldownRemaining = cooldown.normal - timeElapsedSinceEaten; - return Math.max(cooldownRemaining, 0); // Return 0 if the cooldown has expired - } - return 0; - } - - /** - * Get player's current enchanted golden apple cooldown - * - * @param playerUUID The UUID of the player to check the cooldown for. - * @return The remaining cooldown time in seconds, or 0 if there is no cooldown, or it has expired. - */ - public long getNappleCooldown(UUID playerUUID) { - final LastEaten lastEatenInfo = lastEaten.get(playerUUID); - if (lastEatenInfo != null && lastEatenInfo.lastEnchantedEaten != null) { - long timeElapsedSinceEaten = Duration.between(lastEatenInfo.lastEnchantedEaten, Instant.now()).getSeconds(); - long cooldownRemaining = cooldown.enchanted - timeElapsedSinceEaten; - return Math.max(cooldownRemaining, 0); // Return 0 if the cooldown has expired - } - return 0; - } - - private static class LastEaten { - private Instant lastNormalEaten; - private Instant lastEnchantedEaten; - - private Optional getForItem(ItemStack item) { - return ENCHANTED_GOLDEN_APPLE.isSame(item) - ? Optional.ofNullable(lastEnchantedEaten) - : Optional.ofNullable(lastNormalEaten); - } - - private Optional getNewestEatTime() { - if (lastEnchantedEaten == null) { - return Optional.ofNullable(lastNormalEaten); - } - if (lastNormalEaten == null) { - return Optional.of(lastEnchantedEaten); - } - return Optional.of( - lastNormalEaten.compareTo(lastEnchantedEaten) < 0 ? lastEnchantedEaten : lastNormalEaten - ); - } - - private void setForItem(ItemStack item) { - if (ENCHANTED_GOLDEN_APPLE.isSame(item)) { - lastEnchantedEaten = Instant.now(); - } else { - lastNormalEaten = Instant.now(); - } - } - } - - private record Cooldown(long normal, long enchanted, boolean sharedCooldown) { - private long getCooldownForItem(ItemStack item) { - return ENCHANTED_GOLDEN_APPLE.isSame(item) ? enchanted : normal; - } - - boolean isOnCooldown(ItemStack item, LastEaten lastEaten) { - return (sharedCooldown ? lastEaten.getNewestEatTime() : lastEaten.getForItem(item)) - .map(it -> ChronoUnit.SECONDS.between(it, Instant.now())) - .map(it -> it < getCooldownForItem(item)) - .orElse(false); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleNoLapisEnchantments.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleNoLapisEnchantments.java deleted file mode 100644 index c891271a..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleNoLapisEnchantments.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.versions.materials.MaterialRegistry; -import kernitus.plugin.OldCombatMechanics.versions.materials.VersionedMaterial; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.enchantment.EnchantItemEvent; -import org.bukkit.event.inventory.*; -import org.bukkit.inventory.EnchantingInventory; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.permissions.Permissible; - -/** - * Allows enchanting without needing lapis. - */ -public class ModuleNoLapisEnchantments extends OCMModule { - - private final VersionedMaterial lapisLazuli; - - public ModuleNoLapisEnchantments(OCMMain plugin) { - super(plugin, "no-lapis-enchantments"); - - lapisLazuli = MaterialRegistry.LAPIS_LAZULI; - } - - @EventHandler - public void onEnchant(EnchantItemEvent e) { - final Player player = e.getEnchanter(); - if (!isEnabled(player)) return; - - if (hasNoPermission(player)) return; - - final EnchantingInventory ei = (EnchantingInventory) e.getInventory(); //Not checking here because how else would event be fired? - ei.setSecondary(getLapis()); - } - - @EventHandler - public void onInventoryClick(InventoryClickEvent e) { - if (!isEnabled(e.getWhoClicked())) return; - - if (e.getInventory().getType() != InventoryType.ENCHANTING) return; - - if (hasNoPermission(e.getWhoClicked())) return; - - final ItemStack item = e.getCurrentItem(); - - if (item == null) return; - - // prevent taking it out - if (lapisLazuli.isSame(item) && e.getRawSlot() == 1) { - e.setCancelled(true); - } else if (e.getCursor() != null && lapisLazuli.isSame(e.getCursor()) && e.getClick() == ClickType.DOUBLE_CLICK) { - e.setCancelled(true); - } - } - - @EventHandler - public void onInventoryClose(InventoryCloseEvent e) { - if (!isEnabled(e.getPlayer())) return; - - final Inventory inventory = e.getInventory(); - if (inventory == null || inventory.getType() != InventoryType.ENCHANTING) return; - - // always clear it, so nothing is left over in the table - ((EnchantingInventory) inventory).setSecondary(null); - } - - @EventHandler - public void onInventoryOpen(InventoryOpenEvent e) { - fillUpEnchantingTable(e.getPlayer(), e.getInventory()); - } - - private void fillUpEnchantingTable(HumanEntity player, Inventory inventory) { - if (!isEnabled(player)) return; - - if (inventory == null || inventory.getType() != InventoryType.ENCHANTING || hasNoPermission(player)) return; - ((EnchantingInventory) inventory).setSecondary(getLapis()); - } - - private ItemStack getLapis() { - final ItemStack lapis = lapisLazuli.newInstance(); - lapis.setAmount(64); - return lapis; - } - - private boolean hasNoPermission(Permissible player) { - return isSettingEnabled("usePermission") && !player.hasPermission("oldcombatmechanics.nolapis"); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourDurability.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourDurability.java deleted file mode 100644 index a2031dbb..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourDurability.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.player.PlayerItemDamageEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.*; -import java.util.stream.Collectors; - -public class ModuleOldArmourDurability extends OCMModule { - - private final Map> explosionDamaged = new WeakHashMap<>(); - - public ModuleOldArmourDurability(OCMMain plugin) { - super(plugin, "old-armour-durability"); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onItemDamage(PlayerItemDamageEvent e) { - final Player player = e.getPlayer(); - - if (!isEnabled(player)) return; - final ItemStack item = e.getItem(); - final Material itemType = item.getType(); - - // Check if it's a piece of armour they're currently wearing - if (Arrays.stream(player.getInventory().getArmorContents()) - .noneMatch(armourPiece -> armourPiece != null && - armourPiece.getType() == itemType && - armourPiece.getType() != Material.ELYTRA // ignore elytra as it doesn't provide any protection anyway - )) return; - - final UUID uuid = player.getUniqueId(); - if (explosionDamaged.containsKey(uuid)) { - final List armour = explosionDamaged.get(uuid); - // ItemStack.equals() checks material, durability and quantity to make sure nothing changed in the meantime - // We're checking all the pieces this way just in case they're wearing two helmets or something strange - final List matchedPieces = armour.stream().filter(piece -> piece.equals(item)).toList(); - armour.removeAll(matchedPieces); - debug("Item matched explosion, ignoring...", player); - if (!matchedPieces.isEmpty()) return; - } - - int reduction = module().getInt("reduction"); - - // 60 + (40 / (level + 1) ) % chance that durability is reduced (for each point of durability) - final int damageChance = 60 + (40 / (item.getEnchantmentLevel(EnchantmentCompat.UNBREAKING.get()) + 1)); - final Random random = new Random(); - final int randomInt = random.nextInt(100); // between 0 (inclusive) and 100 (exclusive) - if (randomInt >= damageChance) - reduction = 0; - - debug("Item damaged: " + itemType + " Damage: " + e.getDamage() + " Changed to: " + reduction, player); - e.setDamage(reduction); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerExplosionDamage(EntityDamageEvent e) { - if (e.isCancelled()) return; - if (e.getEntityType() != EntityType.PLAYER) return; - final EntityDamageEvent.DamageCause cause = e.getCause(); - if (cause != EntityDamageEvent.DamageCause.BLOCK_EXPLOSION && - cause != EntityDamageEvent.DamageCause.ENTITY_EXPLOSION) return; - - final Player player = (Player) e.getEntity(); - final UUID uuid = player.getUniqueId(); - final List armour = Arrays.stream(player.getInventory().getArmorContents()).filter(Objects::nonNull).collect(Collectors.toList()); - explosionDamaged.put(uuid, armour); - - Bukkit.getScheduler().runTaskLater(plugin, () -> { - explosionDamaged.remove(uuid); - debug("Removed from explosion set!", player); - }, 1L); // This delay seems enough for the durability events to fire - - debug("Detected explosion!", player); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourStrength.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourStrength.java deleted file mode 100644 index 80b591ea..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourStrength.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.damage.DefenceUtils; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; - -import java.util.Arrays; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Reverts the armour strength changes to 1.8 calculations, including enchantments. - * Also recalculates resistance and absorption accordingly. - *

- * It is based on this revision - * of the minecraft wiki. - */ -public class ModuleOldArmourStrength extends OCMModule { -// Defence order is armour defence points -> resistance -> armour enchants -> absorption - - private boolean randomness; - - public ModuleOldArmourStrength(OCMMain plugin) { - super(plugin, "old-armour-strength"); - reload(); - } - - @Override - public void reload() { - randomness = module().getBoolean("randomness"); - } - - @SuppressWarnings("deprecation") - @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) - public void onEntityDamage(EntityDamageEvent e) { - // 1.8 NMS: Damage = (damage after blocking * (25 - total armour strength)) / 25 - if (!(e.getEntity() instanceof LivingEntity)) return; - - final LivingEntity damagedEntity = (LivingEntity) e.getEntity(); - - // If there was an attacker, and he does not have this module enabled, return - if (e.getCause() == EntityDamageEvent.DamageCause.ENTITY_ATTACK && e instanceof EntityDamageByEntityEvent) { - final Entity damager = ((EntityDamageByEntityEvent) e).getDamager(); - if(!isEnabled(damager, damagedEntity)) return; - } - - final Map damageModifiers = - Arrays.stream(EntityDamageEvent.DamageModifier.values()) - .filter(e::isApplicable) - .collect(Collectors.toMap(m -> m, e::getDamage)); - - DefenceUtils.calculateDefenceDamageReduction(damagedEntity, damageModifiers, e.getCause(), randomness); - - // Set the modifiers back to the event - damageModifiers.forEach(e::setDamage); - - debug("BASE: " + damageModifiers.get(EntityDamageEvent.DamageModifier.BASE)); - debug("BLOCKING: " + damageModifiers.get(EntityDamageEvent.DamageModifier.BLOCKING)); - debug("ARMOUR: " + damageModifiers.get(EntityDamageEvent.DamageModifier.ARMOR)); - debug("RESISTANCE: " + damageModifiers.get(EntityDamageEvent.DamageModifier.RESISTANCE)); - debug("ARMOUR ENCHS: " + damageModifiers.get(EntityDamageEvent.DamageModifier.MAGIC)); - debug("ABSORPTION: " + damageModifiers.get(EntityDamageEvent.DamageModifier.ABSORPTION)); - - final double finalDamage = damageModifiers.values().stream().reduce(0.0, Double::sum); - debug("Final damage after defence calc: " + finalDamage); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldBrewingStand.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldBrewingStand.java deleted file mode 100644 index 80c4789e..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldBrewingStand.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bukkit.Location; -import org.bukkit.block.Block; -import org.bukkit.block.BlockState; -import org.bukkit.block.BrewingStand; -import org.bukkit.event.EventHandler; -import org.bukkit.event.inventory.InventoryOpenEvent; -import org.bukkit.inventory.Inventory; - -/** - * Makes brewing stands not require fuel. - */ -public class ModuleOldBrewingStand extends OCMModule { - - public ModuleOldBrewingStand(OCMMain plugin) { - super(plugin, "old-brewing-stand"); - } - - @EventHandler - public void onInventoryOpen(InventoryOpenEvent e) { - // Set max fuel when they open brewing stand - // If they run out, they can just close and open it again - if (!isEnabled(e.getPlayer())) return; - - final Inventory inventory = e.getInventory(); - final Location location = inventory.getLocation(); - if (location == null) return; - - final Block block = location.getBlock(); - final BlockState blockState = block.getState(); - - if (!(blockState instanceof BrewingStand)) return; - - final BrewingStand brewingStand = (BrewingStand) blockState; - - brewingStand.setFuelLevel(20); - brewingStand.update(); - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldBurnDelay.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldBurnDelay.java deleted file mode 100644 index abff8d6c..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldBurnDelay.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bukkit.entity.Entity; -import org.bukkit.event.EventHandler; -import org.bukkit.event.entity.EntityDamageEvent; - -/** - * Bring back old fire burning delay behaviour - */ -public class ModuleOldBurnDelay extends OCMModule { - - private int fireTicks; - - public ModuleOldBurnDelay(OCMMain plugin) { - super(plugin, "old-burn-delay"); - reload(); - } - - @Override - public void reload() { - fireTicks = module().getInt("fire-ticks"); - } - - @EventHandler - public void onFireTick(EntityDamageEvent e) { - if (e.getCause() == EntityDamageEvent.DamageCause.FIRE) { - final Entity entity = e.getEntity(); - if(!isEnabled(entity)) return; - - entity.setFireTicks(fireTicks); - debug("Setting fire ticks to " + fireTicks, entity); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldCriticalHits.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldCriticalHits.java deleted file mode 100644 index 54a14c7f..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldCriticalHits.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.damage.OCMEntityDamageByEntityEvent; -import org.bukkit.event.EventHandler; - -public class ModuleOldCriticalHits extends OCMModule { - - private boolean allowSprinting; - private double multiplier; - - public ModuleOldCriticalHits(OCMMain plugin) { - super(plugin, "old-critical-hits"); - reload(); - } - - @Override - public void reload() { - allowSprinting = module().getBoolean("allowSprinting", true); - multiplier = module().getDouble("multiplier", 1.5); - } - - @EventHandler - public void onOCMDamage(OCMEntityDamageByEntityEvent e) { - if (!isEnabled(e.getDamager(), e.getDamagee())) return; - - // In 1.9, a critical hit requires the player not to be sprinting - if (e.was1_8Crit() && (allowSprinting || !e.wasSprinting())) - e.setCriticalMultiplier(multiplier); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldPotionEffects.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldPotionEffects.java deleted file mode 100644 index a7c26876..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldPotionEffects.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils; -import kernitus.plugin.OldCombatMechanics.utilities.damage.OCMEntityDamageByEntityEvent; -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionDurations; -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionTypeCompat; -import org.bukkit.Material; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockDispenseEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerItemConsumeEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.PotionMeta; -import org.bukkit.potion.PotionData; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.potion.PotionType; - -import java.util.List; -import java.util.Map; -import java.util.Set; - - -/** - * Allows configurable potion effect durations. - */ -public class ModuleOldPotionEffects extends OCMModule { - private static final Set EXCLUDED_POTION_TYPES = Set.of( - // Base potions without any effect - new PotionTypeCompat("AWKWARD"), - new PotionTypeCompat("MUNDANE"), - new PotionTypeCompat("THICK"), - new PotionTypeCompat("WATER"), - // Instant potions with no further effects - new PotionTypeCompat("HARMING"), - new PotionTypeCompat("STRONG_HARMING"), - new PotionTypeCompat("HEALING"), - new PotionTypeCompat("STRONG_HEALING"), - // This type doesn't exist anymore >1.20.5, is specially handled in compat class - new PotionTypeCompat("UNCRAFTABLE") - ); - - private Map durations; - - public ModuleOldPotionEffects(OCMMain plugin) { - super(plugin, "old-potion-effects"); - - reload(); - } - - @Override - public void reload() { - durations = ConfigUtils.loadPotionDurationsList(module()); - } - - /** - * Change the duration using values defined in config for drinking potions - */ - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - public void onPlayerDrinksPotion(PlayerItemConsumeEvent event) { - final Player player = event.getPlayer(); - if (!isEnabled(player)) return; - - final ItemStack potionItem = event.getItem(); - if (potionItem.getType() != Material.POTION) return; - - adjustPotion(potionItem, false); - event.setItem(potionItem); - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onPotionDispense(BlockDispenseEvent event) { - if (!isEnabled(event.getBlock().getWorld())) return; - - final ItemStack item = event.getItem(); - final Material material = item.getType(); - - if (material == Material.SPLASH_POTION || material == Material.LINGERING_POTION) - adjustPotion(item, true); - } - - // We change the potion on-the-fly just as it's thrown to be able to change the effect - @EventHandler(priority = EventPriority.HIGHEST) - public void onPotionThrow(PlayerInteractEvent event) { - final Player player = event.getPlayer(); - if (!isEnabled(player)) return; - - final Action action = event.getAction(); - if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) return; - - final ItemStack item = event.getItem(); - if (item == null) return; - - final Material material = item.getType(); - if (material == Material.SPLASH_POTION || material == Material.LINGERING_POTION) - adjustPotion(item, true); - } - - /** - * Sets custom potion duration and effects - * - * @param potionItem The potion item with adjusted duration and effects - */ - private void adjustPotion(ItemStack potionItem, boolean splash) { - final PotionMeta potionMeta = (PotionMeta) potionItem.getItemMeta(); - if (potionMeta == null) return; - - final PotionTypeCompat potionTypeCompat = PotionTypeCompat.fromPotionMeta(potionMeta); - - if (EXCLUDED_POTION_TYPES.contains(potionTypeCompat)) return; - - final Integer duration = getPotionDuration(potionTypeCompat, splash); - if (duration == null) { - debug("Potion type " + potionTypeCompat.getNewName() + " not found in config, leaving as is..."); - return; - } - - int amplifier = potionTypeCompat.isStrong() ? 1 : 0; - - if (potionTypeCompat.equals(new PotionTypeCompat("WEAKNESS"))) { - // Set level to 0 so that it doesn't prevent the EntityDamageByEntityEvent from being called - // due to damage being lower than 0 as some 1.9 weapons deal less damage - amplifier = -1; - } - - List potionEffects; - final PotionType potionType = potionTypeCompat.getType(); - try { - potionEffects = potionType.getPotionEffects().stream().map(PotionEffect::getType).toList(); - } catch (NoSuchMethodError e) { - potionEffects = List.of(potionType.getEffectType()); - } - - for (PotionEffectType effectType : potionEffects) { - potionMeta.addCustomEffect(new PotionEffect(effectType, duration, amplifier), false); - } - - try { // For >=1.20 - potionMeta.setBasePotionType(PotionType.WATER); - } catch (NoSuchMethodError e) { - potionMeta.setBasePotionData(new PotionData(PotionType.WATER)); - } - - potionItem.setItemMeta(potionMeta); - } - - - @EventHandler(ignoreCancelled = true) - public void onDamageByEntity(OCMEntityDamageByEntityEvent event) { - final Entity damager = event.getDamager(); - if (!isEnabled(damager, event.getDamagee())) return; - - if (event.hasWeakness()) { - event.setIsWeaknessModifierMultiplier(module().getBoolean("weakness.multiplier")); - final double newWeaknessModifier = module().getDouble("weakness.modifier"); - event.setWeaknessModifier(newWeaknessModifier); - event.setWeaknessLevel(1); - debug("Old weakness modifier: " + event.getWeaknessLevel() + - " New: " + newWeaknessModifier, damager); - } - - final double strengthModifier = event.getStrengthModifier(); - - if (strengthModifier > 0) { - event.setIsStrengthModifierMultiplier(module().getBoolean("strength.multiplier")); - event.setIsStrengthModifierAddend(module().getBoolean("strength.addend")); - final double newStrengthModifier = module().getDouble("strength.modifier"); - event.setStrengthModifier(newStrengthModifier); - debug("Old strength modifier: " + strengthModifier + " New: " + newStrengthModifier, damager); - } - } - - private Integer getPotionDuration(PotionTypeCompat potionTypeCompat, boolean splash) { - final PotionDurations potionDurations = durations.get(potionTypeCompat); - if (potionDurations == null) return null; - final int duration = splash ? potionDurations.splash() : potionDurations.drinkable(); - - debug("Potion type: " + potionTypeCompat.getNewName() + " Duration: " + duration + " ticks"); - - return duration; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldToolDamage.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldToolDamage.java deleted file mode 100644 index 8c4c459a..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleOldToolDamage.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils; -import kernitus.plugin.OldCombatMechanics.utilities.damage.NewWeaponDamage; -import kernitus.plugin.OldCombatMechanics.utilities.damage.OCMEntityDamageByEntityEvent; -import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages; -import org.bukkit.Material; -import org.bukkit.entity.Entity; -import org.bukkit.event.EventHandler; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.Arrays; -import java.util.Locale; - -/** - * Restores old tool damage. - */ -public class ModuleOldToolDamage extends OCMModule { - - private static final String[] WEAPONS = {"sword", "axe", "pickaxe", "spade", "shovel", "hoe"}; - private boolean oldSharpness; - - public ModuleOldToolDamage(OCMMain plugin) { - super(plugin, "old-tool-damage"); - reload(); - } - - @Override - public void reload() { - oldSharpness = module().getBoolean("old-sharpness", true); - } - - @EventHandler(ignoreCancelled = true) - public void onEntityDamaged(OCMEntityDamageByEntityEvent event) { - final Entity damager = event.getDamager(); - if (event.getCause() == EntityDamageEvent.DamageCause.THORNS) return; - - if (!isEnabled(damager, event.getDamagee())) return; - - final ItemStack weapon = event.getWeapon(); - final Material weaponMaterial = weapon.getType(); - debug("Weapon material: " + weaponMaterial); - - if (!isWeapon(weaponMaterial)) return; - - // If damage was not what we expected, ignore it because it's probably a custom weapon or from another plugin - final double oldBaseDamage = event.getBaseDamage(); - final double expectedBaseDamage = NewWeaponDamage.getDamage(weaponMaterial); - // We check difference as calculation inaccuracies can make it not match - if (Math.abs(oldBaseDamage - expectedBaseDamage) > 0.0001) { - debug("Expected " + expectedBaseDamage + " got " + oldBaseDamage + " ignoring weapon..."); - return; - } - - final double newWeaponBaseDamage = WeaponDamages.getDamage(weaponMaterial); - if (newWeaponBaseDamage <= 0) { - debug("Unknown tool type: " + weaponMaterial, damager); - return; - } - - event.setBaseDamage(newWeaponBaseDamage); - Messenger.debug("Old tool damage: " + oldBaseDamage + " New: " + newWeaponBaseDamage); - - - // Set sharpness to 1.8 damage value - final int sharpnessLevel = event.getSharpnessLevel(); - double newSharpnessDamage = oldSharpness ? - DamageUtils.getOldSharpnessDamage(sharpnessLevel) : - DamageUtils.getNewSharpnessDamage(sharpnessLevel); - - debug("Old sharpness damage: " + event.getSharpnessDamage() + " New: " + newSharpnessDamage, damager); - event.setSharpnessDamage(newSharpnessDamage); - - // The mob enchantments damage remains the same and is linear, no need to recalculate it - } - - private boolean isWeapon(Material material) { - return Arrays.stream(WEAPONS).anyMatch(type -> isOfType(material, type)); - } - - private boolean isOfType(Material mat, String type) { - return mat.toString().endsWith("_" + type.toUpperCase(Locale.ROOT)); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModulePlayerKnockback.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModulePlayerKnockback.java deleted file mode 100644 index 1d4ddce8..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModulePlayerKnockback.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeInstance; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.Entity; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerVelocityEvent; -import org.bukkit.inventory.EntityEquipment; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.Vector; - -import java.util.Map; -import java.util.UUID; -import java.util.WeakHashMap; - -/** - * Reverts knockback formula to 1.8. - * Also disables netherite knockback resistance. - */ -public class ModulePlayerKnockback extends OCMModule { - - private double knockbackHorizontal; - private double knockbackVertical; - private double knockbackVerticalLimit; - private double knockbackExtraHorizontal; - private double knockbackExtraVertical; - private boolean netheriteKnockbackResistance; - - private final Map playerKnockbackHashMap = new WeakHashMap<>(); - - public ModulePlayerKnockback(OCMMain plugin) { - super(plugin, "old-player-knockback"); - reload(); - } - - @Override - public void reload() { - knockbackHorizontal = module().getDouble("knockback-horizontal", 0.4); - knockbackVertical = module().getDouble("knockback-vertical", 0.4); - knockbackVerticalLimit = module().getDouble("knockback-vertical-limit", 0.4); - knockbackExtraHorizontal = module().getDouble("knockback-extra-horizontal", 0.5); - knockbackExtraVertical = module().getDouble("knockback-extra-vertical", 0.1); - netheriteKnockbackResistance = module().getBoolean("enable-knockback-resistance", false) && Reflector.versionIsNewerOrEqualTo(1, 16, 0); - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent e) { - playerKnockbackHashMap.remove(e.getPlayer().getUniqueId()); - } - - // Vanilla does its own knockback, so we need to set it again. - // priority = lowest because we are ignoring the existing velocity, which could break other plugins - @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) - public void onPlayerVelocityEvent(PlayerVelocityEvent event) { - final UUID uuid = event.getPlayer().getUniqueId(); - if (!playerKnockbackHashMap.containsKey(uuid)) return; - event.setVelocity(playerKnockbackHashMap.get(uuid)); - playerKnockbackHashMap.remove(uuid); - } - - @EventHandler - public void onEntityDamage(EntityDamageEvent event) { - // Disable netherite kb, the knockback resistance attribute makes the velocity event not be called - final Entity entity = event.getEntity(); - if (!(entity instanceof Player) || netheriteKnockbackResistance) return; - final Player damagee = (Player) entity; - - // This depends on the attacker's combat mode - if (event.getCause() == EntityDamageEvent.DamageCause.ENTITY_ATTACK - && event instanceof EntityDamageByEntityEvent) { - final Entity damager = ((EntityDamageByEntityEvent) event).getDamager(); - if(!isEnabled(damager)) return; - } else { - if(!isEnabled(damagee)) return; - } - - final AttributeInstance attribute = damagee.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE); - attribute.getModifiers().forEach(attribute::removeModifier); - } - - // Monitor priority because we don't modify anything here, but apply on velocity change event - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onEntityDamageEntity(EntityDamageByEntityEvent event) { - final Entity damager = event.getDamager(); - if (!(damager instanceof LivingEntity)) return; - final LivingEntity attacker = (LivingEntity) damager; - - final Entity damagee = event.getEntity(); - if (!(damagee instanceof Player)) return; - final Player victim = (Player) damagee; - - if (event.getCause() != EntityDamageEvent.DamageCause.ENTITY_ATTACK) return; - if (event.getDamage(EntityDamageEvent.DamageModifier.BLOCKING) > 0) return; - - if(attacker instanceof HumanEntity){ - if (!isEnabled(attacker)) return; - } else if(!isEnabled(victim)) return; - - // Figure out base knockback direction - Location attackerLocation = attacker.getLocation(); - Location victimLocation = victim.getLocation(); - double d0 = attackerLocation.getX() - victimLocation.getX(); - double d1; - - for (d1 = attackerLocation.getZ() - victimLocation.getZ(); - d0 * d0 + d1 * d1 < 1.0E-4D; d1 = (Math.random() - Math.random()) * 0.01D) { - d0 = (Math.random() - Math.random()) * 0.01D; - } - - final double magnitude = Math.sqrt(d0 * d0 + d1 * d1); - - // Get player knockback before any friction is applied - final Vector playerVelocity = victim.getVelocity(); - - // Apply friction, then add base knockback - playerVelocity.setX((playerVelocity.getX() / 2) - (d0 / magnitude * knockbackHorizontal)); - playerVelocity.setY((playerVelocity.getY() / 2) + knockbackVertical); - playerVelocity.setZ((playerVelocity.getZ() / 2) - (d1 / magnitude * knockbackHorizontal)); - - // Calculate bonus knockback for sprinting or knockback enchantment levels - final EntityEquipment equipment = attacker.getEquipment(); - if (equipment != null) { - final ItemStack heldItem = equipment.getItemInMainHand().getType() == Material.AIR ? - equipment.getItemInOffHand() : equipment.getItemInMainHand(); - - int bonusKnockback = heldItem.getEnchantmentLevel(Enchantment.KNOCKBACK); - if (attacker instanceof Player && ((Player) attacker).isSprinting()) ++bonusKnockback; - - if (playerVelocity.getY() > knockbackVerticalLimit) playerVelocity.setY(knockbackVerticalLimit); - - if (bonusKnockback > 0) { // Apply bonus knockback - playerVelocity.add(new Vector((-Math.sin(attacker.getLocation().getYaw() * 3.1415927F / 180.0F) * - (float) bonusKnockback * knockbackExtraHorizontal), knockbackExtraVertical, - Math.cos(attacker.getLocation().getYaw() * 3.1415927F / 180.0F) * - (float) bonusKnockback * knockbackExtraHorizontal)); - } - } - - if (netheriteKnockbackResistance) { - // Allow netherite to affect the horizontal knockback. Each piece of armour yields 10% resistance - final double resistance = 1 - victim.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).getValue(); - playerVelocity.multiply(new Vector(resistance, 1, resistance)); - } - - final UUID victimId = victim.getUniqueId(); - - // Knockback is sent immediately in 1.8+, there is no reason to send packets manually - playerKnockbackHashMap.put(victimId, playerVelocity); - - // Sometimes PlayerVelocityEvent doesn't fire, remove data to not affect later events if that happens - Bukkit.getScheduler().runTaskLater(plugin, () -> playerKnockbackHashMap.remove(victimId), 1); - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModulePlayerRegen.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModulePlayerRegen.java deleted file mode 100644 index 93a9511c..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModulePlayerRegen.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.MathsHelper; -import org.bukkit.Bukkit; -import org.bukkit.attribute.Attribute; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityRegainHealthEvent; -import org.bukkit.event.player.PlayerQuitEvent; - -import java.util.Map; -import java.util.UUID; -import java.util.WeakHashMap; - -/** - * Establishes custom health regeneration rules. - * Default values based on 1.8 from wiki - */ -public class ModulePlayerRegen extends OCMModule { - - private final Map healTimes = new WeakHashMap<>(); - - public ModulePlayerRegen(OCMMain plugin) { - super(plugin, "old-player-regen"); - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onRegen(EntityRegainHealthEvent e) { - if (e.getEntityType() != EntityType.PLAYER - || e.getRegainReason() != EntityRegainHealthEvent.RegainReason.SATIATED) - return; - - final Player p = (Player) e.getEntity(); - if (!isEnabled(p)) return; - - final UUID playerId = p.getUniqueId(); - - // We cancel the regen, but saturation and exhaustion need to be adjusted separately - // Exhaustion is modified in the next tick, and saturation in the tick following that (if exhaustion > 4) - e.setCancelled(true); - - // Get exhaustion & saturation values before healing modifies them - final float previousExhaustion = p.getExhaustion(); - final float previousSaturation = p.getSaturation(); - - // Check that it has been at least x seconds since last heal - final long currentTime = System.currentTimeMillis(); - final boolean hasLastHealTime = healTimes.containsKey(playerId); - final long lastHealTime = healTimes.computeIfAbsent(playerId, id -> currentTime); - - debug("Exh: " + previousExhaustion + " Sat: " + previousSaturation + " Time: " + (currentTime - lastHealTime), p); - - // If we're skipping this heal, we must fix the exhaustion in the following tick - if (hasLastHealTime && currentTime - lastHealTime <= module().getLong("interval")) { - Bukkit.getScheduler().runTaskLater(plugin, () -> p.setExhaustion(previousExhaustion), 1L); - return; - } - - final double maxHealth = p.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue(); - final double playerHealth = p.getHealth(); - - if (playerHealth < maxHealth) { - p.setHealth(MathsHelper.clamp(playerHealth + module().getInt("amount"), 0.0, maxHealth)); - healTimes.put(playerId, currentTime); - } - - // Calculate new exhaustion value, must be between 0 and 4. If above, it will reduce the saturation in the following tick. - final float exhaustionToApply = (float) module().getDouble("exhaustion"); - - Bukkit.getScheduler().runTaskLater(plugin, () -> { - // We do this in the next tick because bukkit doesn't stop the exhaustion change when cancelling the event - p.setExhaustion(previousExhaustion + exhaustionToApply); - debug("Exh before: " + previousExhaustion + " Now: " + p.getExhaustion() + - " Sat now: " + previousSaturation, p); - }, 1L); - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent e) { - healTimes.remove(e.getPlayer().getUniqueId()); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleProjectileKnockback.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleProjectileKnockback.java deleted file mode 100644 index 9e4767c3..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleProjectileKnockback.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bukkit.entity.EntityType; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; - -import java.util.Locale; - -/** - * Adds knockback to eggs, snowballs and ender pearls. - */ -public class ModuleProjectileKnockback extends OCMModule { - - public ModuleProjectileKnockback(OCMMain plugin) { - super(plugin, "projectile-knockback"); - } - - @EventHandler(priority = EventPriority.NORMAL) - public void onEntityHit(EntityDamageByEntityEvent e) { - if (!isEnabled(e.getDamager(), e.getEntity())) return; - - final EntityType type = e.getDamager().getType(); - - switch (type) { - case SNOWBALL: case EGG: case ENDER_PEARL: - if (e.getDamage() == 0.0) { // So we don't override enderpearl fall damage - e.setDamage(module().getDouble("damage." + type.toString().toLowerCase(Locale.ROOT))); - if (e.isApplicable(EntityDamageEvent.DamageModifier.ABSORPTION)) - e.setDamage(EntityDamageEvent.DamageModifier.ABSORPTION, 0); - } - } - - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleShieldDamageReduction.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleShieldDamageReduction.java deleted file mode 100644 index c24938b4..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleShieldDamageReduction.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bukkit.Bukkit; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent.DamageCause; -import org.bukkit.event.entity.EntityDamageEvent.DamageModifier; -import org.bukkit.event.player.PlayerItemDamageEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.*; -import java.util.stream.Collectors; - -/** - * Allows customising the shield damage reduction percentages. - */ -public class ModuleShieldDamageReduction extends OCMModule { - - private int genericDamageReductionAmount, genericDamageReductionPercentage, projectileDamageReductionAmount, projectileDamageReductionPercentage; - private final Map> fullyBlocked = new WeakHashMap<>(); - - public ModuleShieldDamageReduction(OCMMain plugin) { - super(plugin, "shield-damage-reduction"); - reload(); - } - - @Override - public void reload() { - genericDamageReductionAmount = module().getInt("generalDamageReductionAmount", 1); - genericDamageReductionPercentage = module().getInt("generalDamageReductionPercentage", 50); - projectileDamageReductionAmount = module().getInt("projectileDamageReductionAmount", 1); - projectileDamageReductionPercentage = module().getInt("projectileDamageReductionPercentage", 50); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onItemDamage(PlayerItemDamageEvent e) { - final Player player = e.getPlayer(); - if (!isEnabled(player)) return; - final UUID uuid = player.getUniqueId(); - final ItemStack item = e.getItem(); - - if (fullyBlocked.containsKey(uuid)) { - final List armour = fullyBlocked.get(uuid); - // ItemStack.equals() checks material, durability and quantity to make sure nothing changed in the meantime - // We're checking all the pieces this way just in case they're wearing two helmets or something strange - final List matchedPieces = armour.stream().filter(piece -> piece.equals(item)).collect(Collectors.toList()); - armour.removeAll(matchedPieces); - debug("Ignoring armour durability damage due to full block", player); - if (!matchedPieces.isEmpty()) { - e.setCancelled(true); - } - } - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onHit(EntityDamageByEntityEvent e) { - final Entity entity = e.getEntity(); - - if (!(entity instanceof Player)) return; - - final Player player = (Player) entity; - - if (!isEnabled(e.getDamager(), player)) return; - - // Blocking is calculated after base and hard hat, and before armour etc. - final double baseDamage = e.getDamage(DamageModifier.BASE) + e.getDamage(DamageModifier.HARD_HAT); - if (!shieldBlockedDamage(baseDamage, e.getDamage(DamageModifier.BLOCKING))) return; - - final double damageReduction = getDamageReduction(baseDamage, e.getCause()); - e.setDamage(DamageModifier.BLOCKING, -damageReduction); - final double currentDamage = baseDamage - damageReduction; - - debug("Blocking: " + baseDamage + " - " + damageReduction + " = " + currentDamage, player); - debug("Blocking: " + baseDamage + " - " + damageReduction + " = " + currentDamage); - - final UUID uuid = player.getUniqueId(); - - if (currentDamage <= 0) { // Make sure armour is not damaged if fully blocked - final List armour = Arrays.stream(player.getInventory().getArmorContents()).filter(Objects::nonNull).collect(Collectors.toList()); - fullyBlocked.put(uuid, armour); - - Bukkit.getScheduler().runTaskLater(plugin, () -> { - fullyBlocked.remove(uuid); - debug("Removed from fully blocked set!", player); - }, 1L); - } - } - - private double getDamageReduction(double damage, DamageCause damageCause) { - // 1.8 NMS code, where f is damage done, to calculate new damage. - // f = (1.0F + f) * 0.5F; - - // We subtract, to calculate damage reduction instead of new damage - double reduction = damage - (damageCause == DamageCause.PROJECTILE ? projectileDamageReductionAmount : genericDamageReductionAmount); - - // Reduce to percentage - reduction *= (damageCause == DamageCause.PROJECTILE ? projectileDamageReductionPercentage : genericDamageReductionPercentage) / 100.0; - - // Don't reduce by more than the actual damage done - // As far as I can tell this is not checked in 1.8NMS, and if the damage was low enough - // blocking would lead to higher damage. However, this is hardly the desired result. - if (reduction < 0) reduction = 0; - - return reduction; - } - - private boolean shieldBlockedDamage(double attackDamage, double blockingReduction) { - // Only reduce damage if they were hit head on, i.e. the shield blocked some of the damage - // This also takes into account damages that are not blocked by shields - return attackDamage > 0 && blockingReduction < 0; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordBlocking.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordBlocking.java deleted file mode 100644 index 13eb591d..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordBlocking.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.entity.Item; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockCanBuildEvent; -import org.bukkit.event.entity.PlayerDeathEvent; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.player.*; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.scheduler.BukkitTask; - -import java.util.*; - -public class ModuleSwordBlocking extends OCMModule { - - private static final ItemStack SHIELD = new ItemStack(Material.SHIELD); - // Not using WeakHashMaps here, for extra reliability - private final Map storedItems = new HashMap<>(); - private final Map> correspondingTasks = new HashMap<>(); - private int restoreDelay; - - // Only used <1.13, where BlockCanBuildEvent.getPlayer() is not available - private Map lastInteractedBlocks; - - public ModuleSwordBlocking(OCMMain plugin) { - super(plugin, "sword-blocking"); - - if (!Reflector.versionIsNewerOrEqualTo(1, 13, 0)) { - lastInteractedBlocks = new WeakHashMap<>(); - } - } - - @Override - public void reload() { - restoreDelay = module().getInt("restoreDelay", 40); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onBlockPlace(BlockCanBuildEvent e) { - if (e.isBuildable()) return; - - Player player; - - // If <1.13 get player who last interacted with block - if (lastInteractedBlocks != null) { - final Location blockLocation = e.getBlock().getLocation(); - final UUID uuid = lastInteractedBlocks.remove(blockLocation); - player = Bukkit.getServer().getPlayer(uuid); - } else player = e.getPlayer(); - - if (player == null || !isEnabled(player)) return; - - doShieldBlock(player); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onRightClick(PlayerInteractEvent e) { - final Action action = e.getAction(); - final Player player = e.getPlayer(); - - if (!isEnabled(player)) return; - - if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; - // If they clicked on an interactive block, the 2nd event with the offhand won't fire - // This is also the case if the main hand item was used, e.g. a bow - // TODO right-clicking on a mob also only fires one hand - if (action == Action.RIGHT_CLICK_BLOCK && e.getHand() == EquipmentSlot.HAND) return; - if (e.isBlockInHand()) { - if (lastInteractedBlocks != null) { - final Block clickedBlock = e.getClickedBlock(); - if (clickedBlock != null) - lastInteractedBlocks.put(clickedBlock.getLocation(), player.getUniqueId()); - } - return; // Handle failed block place in separate listener - } - - doShieldBlock(player); - } - - private void doShieldBlock(Player player) { - final PlayerInventory inventory = player.getInventory(); - - final ItemStack mainHandItem = inventory.getItemInMainHand(); - final ItemStack offHandItem = inventory.getItemInOffHand(); - - if (!isHoldingSword(mainHandItem.getType())) return; - - if (module().getBoolean("use-permission") && - !player.hasPermission("oldcombatmechanics.swordblock")) return; - - final UUID id = player.getUniqueId(); - - if (!isPlayerBlocking(player)) { - if (hasShield(inventory)) return; - debug("Storing " + offHandItem, player); - storedItems.put(id, offHandItem); - - inventory.setItemInOffHand(SHIELD); - // Force an inventory update to avoid ghost items - player.updateInventory(); - } - scheduleRestore(player); - } - - @EventHandler - public void onHotBarChange(PlayerItemHeldEvent e) { - restore(e.getPlayer(), true); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onWorldChange(PlayerChangedWorldEvent e) { - restore(e.getPlayer(), true); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerLogout(PlayerQuitEvent e) { - restore(e.getPlayer(), true); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerDeath(PlayerDeathEvent e) { - final Player p = e.getEntity(); - final UUID id = p.getUniqueId(); - if (!areItemsStored(id)) return; - - //TODO what if they legitimately had a shield? - e.getDrops().replaceAll(item -> - item.getType() == Material.SHIELD ? - storedItems.remove(id) : item - ); - - // Handle keepInventory = true - restore(p, true); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent e) { - if (areItemsStored(e.getPlayer().getUniqueId())) - e.setCancelled(true); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onInventoryClick(InventoryClickEvent e) { - if (e.getWhoClicked() instanceof Player player) { - if (areItemsStored(player.getUniqueId())) { - final ItemStack cursor = e.getCursor(); - final ItemStack current = e.getCurrentItem(); - if (cursor != null && cursor.getType() == Material.SHIELD || - current != null && current.getType() == Material.SHIELD) { - e.setCancelled(true); - restore(player, true); - } - } - } - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onItemDrop(PlayerDropItemEvent e) { - final Item is = e.getItemDrop(); - final Player p = e.getPlayer(); - - if (areItemsStored(p.getUniqueId()) && is.getItemStack().getType() == Material.SHIELD) { - e.setCancelled(true); - restore(p); - } - } - - private void restore(Player player) { - restore(player, false); - } - - private void restore(Player p, boolean force) { - final UUID id = p.getUniqueId(); - - tryCancelTask(id); - - if (!areItemsStored(id)) return; - - // If they are still blocking with the shield, postpone restoring - if (!force && isPlayerBlocking(p)) scheduleRestore(p); - else p.getInventory().setItemInOffHand(storedItems.remove(id)); - } - - private void tryCancelTask(UUID id) { - Optional.ofNullable(correspondingTasks.remove(id)) - .ifPresent(tasks -> tasks.forEach(BukkitTask::cancel)); - } - - private void scheduleRestore(Player p) { - final UUID id = p.getUniqueId(); - tryCancelTask(id); - - final BukkitTask removeItem = Bukkit.getScheduler() - .runTaskLater(plugin, () -> restore(p), restoreDelay); - - final BukkitTask checkBlocking = Bukkit.getScheduler() - .runTaskTimer(plugin, () -> { - if (!isPlayerBlocking(p)) - restore(p); - }, 10L, 2L); - - final List tasks = new ArrayList<>(2); - tasks.add(removeItem); - tasks.add(checkBlocking); - correspondingTasks.put(p.getUniqueId(), tasks); - } - - private boolean areItemsStored(UUID uuid) { - return storedItems.containsKey(uuid); - } - - /** - * Checks whether player is blocking or they have just begun to and shield is not fully up yet. - */ - private boolean isPlayerBlocking(Player player) { - return player.isBlocking() || - (Reflector.versionIsNewerOrEqualTo(1, 11, 0) && player.isHandRaised() - && hasShield(player.getInventory()) - ); - } - - private boolean hasShield(PlayerInventory inventory) { - return inventory.getItemInOffHand().getType() == Material.SHIELD; - } - - private boolean isHoldingSword(Material mat) { - return mat.toString().endsWith("_SWORD"); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweep.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweep.java deleted file mode 100644 index bf3bf347..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweep.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.damage.NewWeaponDamage; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitTask; - -import java.util.ArrayList; -import java.util.List; - -/** - * A module to disable the sweep attack. - */ -public class ModuleSwordSweep extends OCMModule { - - private final List sweepLocations = new ArrayList<>(); - private EntityDamageEvent.DamageCause sweepDamageCause; - private BukkitTask task; - - public ModuleSwordSweep(OCMMain plugin) { - super(plugin, "disable-sword-sweep"); - - try { - // Available from 1.11 onwards - sweepDamageCause = EntityDamageEvent.DamageCause.valueOf("ENTITY_SWEEP_ATTACK"); - } catch (IllegalArgumentException e) { - sweepDamageCause = null; - } - - reload(); - } - - @Override - public void reload() { - // we didn't set anything up in the first place - if (sweepDamageCause != null) return; - - if (task != null) task.cancel(); - - task = Bukkit.getScheduler().runTaskTimer(plugin, sweepLocations::clear, 0, 1); - } - - - //Changed from HIGHEST to LOWEST to support DamageIndicator plugin - @EventHandler(priority = EventPriority.LOWEST) - public void onEntityDamaged(EntityDamageByEntityEvent e) { - final Entity damager = e.getDamager(); - - if (!(damager instanceof Player)) return; - if (!isEnabled(damager, e.getEntity())) return; - - if (sweepDamageCause != null) { - if (e.getCause() == sweepDamageCause) { - e.setCancelled(true); - debug("Sweep cancelled", damager); - } - // sweep attack detected or not, we do not need to fall back to the guessing implementation - return; - } - - final Player attacker = (Player) e.getDamager(); - final ItemStack weapon = attacker.getInventory().getItemInMainHand(); - - if (isHoldingSword(weapon.getType())) - onSwordAttack(e, attacker, weapon); - } - - private void onSwordAttack(EntityDamageByEntityEvent e, Player attacker, ItemStack weapon) { - //Disable sword sweep - final Location attackerLocation = attacker.getLocation(); - - int level = 0; - - try { //In a try catch for servers that haven't updated - level = weapon.getEnchantmentLevel(Enchantment.SWEEPING_EDGE); - } catch (NoSuchFieldError ignored) { - } - - final float damage = NewWeaponDamage.getDamage(weapon.getType()) * level / (level + 1) + 1; - - if (e.getDamage() == damage) { - // Possibly a sword-sweep attack - if (sweepLocations.contains(attackerLocation)) { - debug("Cancelling sweep...", attacker); - e.setCancelled(true); - } - } else { - sweepLocations.add(attackerLocation); - } - } - - private boolean isHoldingSword(Material mat) { - return mat.toString().endsWith("_SWORD"); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweepParticles.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweepParticles.java deleted file mode 100644 index 118c06ea..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweepParticles.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import com.comphenix.protocol.PacketType; -import com.comphenix.protocol.ProtocolManager; -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.events.PacketEvent; -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import org.bukkit.plugin.Plugin; - -import java.util.Locale; - -/** - * A module to disable the sweep attack. - */ -public class ModuleSwordSweepParticles extends OCMModule { - - private final ProtocolManager protocolManager = plugin.getProtocolManager(); - private final ParticleListener particleListener = new ParticleListener(plugin); - - public ModuleSwordSweepParticles(OCMMain plugin) { - super(plugin, "disable-sword-sweep-particles"); - - reload(); - } - - @Override - public void reload() { - if (isEnabled()) - protocolManager.addPacketListener(particleListener); - else - protocolManager.removePacketListener(particleListener); - } - - /** - * Hides sweep particles. - */ - private class ParticleListener extends PacketAdapter { - - private boolean disabledDueToError; - - public ParticleListener(Plugin plugin) { - super(plugin, PacketType.Play.Server.WORLD_PARTICLES); - } - - @Override - public void onPacketSending(PacketEvent packetEvent) { - if (disabledDueToError || !isEnabled(packetEvent.getPlayer().getWorld())) - return; - - try { - final PacketContainer packetContainer = packetEvent.getPacket(); - String particleName; - try { - particleName = packetContainer.getNewParticles().read(0).getParticle().name(); - } catch (Exception exception) { - particleName = packetContainer.getParticles().read(0).name(); // for pre 1.13 - } - - if (particleName.toUpperCase(Locale.ROOT).contains("SWEEP")) { - packetEvent.setCancelled(true); - debug("Cancelled sweep particles", packetEvent.getPlayer()); - } - } catch (Exception | ExceptionInInitializerError e) { - disabledDueToError = true; - Messenger.warn( - e, - "Error detecting sweep packets. Please report it along with the following exception " + - "on github." + - "Sweep cancellation should still work, but particles might show up." - ); - } - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/module/OCMModule.java b/src/main/java/kernitus/plugin/OldCombatMechanics/module/OCMModule.java deleted file mode 100644 index 7a91a21f..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/module/OCMModule.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.module; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.Config; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage; -import org.bukkit.World; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.Entity; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.Listener; -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; -import java.util.Locale; -import java.util.Set; - -/** - * A module providing some specific functionality, e.g. restoring fishing rod knockback. - */ -public abstract class OCMModule implements Listener { - - protected OCMMain plugin; - - private final String configName; - private final String moduleName; - - /** - * Creates a new module. - * - * @param plugin the plugin instance - * @param configName the name of the module in the config - */ - protected OCMModule(OCMMain plugin, String configName) { - this.plugin = plugin; - this.configName = configName; - this.moduleName = getClass().getSimpleName(); - } - - /** - * Checks whether this module is globally en/disabled. - * - * @return true if this module is globally enabled - */ - public boolean isEnabled() { - return Config.moduleEnabled(configName, null); - } - - /** - * Checks whether the module is present in the default modeset for the specified world - * - * @param world The world to get the default modeset for - * @return Whether the module is enabled for the found modeset in the given world - */ - public boolean isEnabled(World world) { - return Config.moduleEnabled(configName, world); - } - - /** - * Whether this module should be enabled for this player given his current modeset - */ - public boolean isEnabled(@NotNull HumanEntity humanEntity) { - final World world = humanEntity.getWorld(); - final String modesetName = PlayerStorage.getPlayerData(humanEntity.getUniqueId()).getModesetForWorld(world.getUID()); - - if (modesetName == null) { - debug("No modeset found!", humanEntity); - debug("No modeset found for " + humanEntity.getName()); - return isEnabled(world); - } - - // Check if the modeset contains this module's name - final Set modeset = Config.getModesets().get(modesetName); - return modeset != null && modeset.contains(configName); - } - - public boolean isEnabled(@NotNull Entity entity) { - if (entity instanceof HumanEntity) - return isEnabled((HumanEntity) entity); - return isEnabled(entity.getWorld()); - } - - /** - * Returns if module should be enabled, giving priority to the attacker, if a human. - * If neither entity is a human, checks if module should be enabled in the defender's world. - * - * @param attacker The entity that is performing the attack - * @param defender The entity that is being attacked - * @return Whether the module should be enabled for this particular interaction - */ - public boolean isEnabled(@NotNull Entity attacker, @NotNull Entity defender) { - if (attacker instanceof HumanEntity) return isEnabled((HumanEntity) attacker); - if (defender instanceof HumanEntity) return isEnabled((HumanEntity) defender); - return isEnabled(defender.getWorld()); - } - - /** - * Checks whether a given setting for this module is enabled. - * - * @param name the name of the setting - * @return true if the setting with that name is enabled. Returns false if the setting did not exist. - */ - public boolean isSettingEnabled(String name) { - return plugin.getConfig().getBoolean(configName + "." + name, false); - } - - /** - * Returns the configuration section for this module. - * - * @return the configuration section for this module - */ - public ConfigurationSection module() { - return plugin.getConfig().getConfigurationSection(configName); - } - - /** - * Called when the plugin is reloaded. Should re-read all relevant config keys and other resources that might have - * changed. - */ - public void reload() { - // Intentionally left blank! Meant for individual modules to use. - } - - /** - * Called when player changes modeset. Re-apply any more permanent changes - * depending on result of isEnabled(player). - * - * @param player The player that changed modeset - */ - public void onModesetChange(Player player) { - // Intentionally left blank! Meant for individual modules to use. - } - - /** - * Outputs a debug message. - * - * @param text the message text - */ - protected void debug(String text) { - Messenger.debug("[" + moduleName + "] " + text); - } - - /** - * Sends a debug message to the given command sender. - * - * @param text the message text - * @param sender the sender to send it to - */ - protected void debug(String text, CommandSender sender) { - if (Config.debugEnabled()) { - Messenger.sendNoPrefix(sender, "&8&l[&fDEBUG&8&l][&f" + moduleName + "&8&l]&7 " + text); - } - } - - @Override - public String toString() { - return Arrays.stream(configName.split("-")) - .map(word -> Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase(Locale.ROOT)) - .reduce((a, b) -> a + " " + b) - .orElse(configName); - } - - /** - * Get the module's name, as taken from the class name - * - * @return The module name, e.g. ModuleDisableAttackCooldown - */ - public String getModuleName() { - return moduleName; - } - - public String getConfigName() { - return configName; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/FakePlayer.java b/src/main/java/kernitus/plugin/OldCombatMechanics/tester/FakePlayer.java deleted file mode 100644 index 115e918e..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/FakePlayer.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.tester; - -import com.mojang.authlib.GameProfile; -import com.mojang.datafixers.util.Pair; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.embedded.EmbeddedChannel; -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import net.minecraft.network.Connection; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.PacketFlow; -import net.minecraft.network.protocol.game.ClientboundAddPlayerPacket; -import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; -import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.server.network.ServerGamePacketListenerImpl; -import net.minecraft.server.players.PlayerList; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.entity.EquipmentSlot; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.GameType; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.craftbukkit.v1_19_R1.CraftServer; -import org.bukkit.craftbukkit.v1_19_R1.CraftWorld; -import org.bukkit.craftbukkit.v1_19_R1.entity.CraftLivingEntity; -import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer; -import org.bukkit.craftbukkit.v1_19_R1.inventory.CraftItemStack; -import org.bukkit.entity.Player; -import org.bukkit.event.player.AsyncPlayerPreLoginEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import xyz.jpenilla.reflectionremapper.ReflectionRemapper; - -import java.lang.reflect.Field; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/* - Resources - - - FakePlayers plugin (1.19 version, had some wrong method names to fix and some things not re-implemented): - https://github.com/gabrianrchua/Fake-Players-Spigot-Plugin/blob/b88b4c33dcf73d9a9a36f6a202b5273712055751/src/main/java/me/KP56/FakePlayers/MultiVersion/v1_19_R1.java - - - Guide on making NMS bots, for a few small tweaks - https://www.spigotmc.org/threads/nms-serverplayer-entityplayer-for-the-1-17-1-18-mojang-mappings-with-fall-damage-and-knockback.551281/ - - - NMS mappings for checking Mojang / Spigot / Obfuscated class, field, and method names - https://mappings.cephx.dev/ - */ - -public class FakePlayer { - - private final UUID uuid; - private final String name; - private ServerPlayer entityPlayer; - private Player bukkitPlayer; - private Integer tickTaskId; - - public FakePlayer() { - uuid = UUID.randomUUID(); - name = uuid.toString().substring(0, 16); - tickTaskId = null; - } - - public UUID getUuid() { - return uuid; - } - - public String getName() { - return name; - } - - public ServerPlayer getServerPlayer() { - return entityPlayer; - } - - public void spawn(Location location) { - ServerLevel worldServer = ((CraftWorld) location.getWorld()).getHandle(); - MinecraftServer mcServer = ((CraftServer) Bukkit.getServer()).getServer(); - - final GameProfile gameProfile = new GameProfile(uuid, name); - this.entityPlayer = new ServerPlayer(mcServer, worldServer, gameProfile, null); - - entityPlayer.connection = new ServerGamePacketListenerImpl(mcServer, new Connection(PacketFlow.CLIENTBOUND), entityPlayer); - entityPlayer.connection.connection.channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter()); - entityPlayer.connection.connection.channel.close(); - - entityPlayer.setGameMode(GameType.SURVIVAL); - - entityPlayer.setPos(location.getX(), location.getY(), location.getZ()); - entityPlayer.setXRot(0); - entityPlayer.setYRot(0); - - try { - final InetAddress ipAddress = InetAddress.getByName("127.0.0.1"); - final AsyncPlayerPreLoginEvent asyncPreLoginEvent = new AsyncPlayerPreLoginEvent(name, ipAddress, uuid); - new Thread(() -> Bukkit.getPluginManager().callEvent(asyncPreLoginEvent)).start(); - - } catch (UnknownHostException e) { - e.printStackTrace(); - } - - // TODO playerloginevent might need to get called separately - //PlayerLoginEvent playerLoginEvent = new PlayerLoginEvent((Player) entityPlayer, "hostname", ipAddress, ipAddress); - - final PlayerList playerList = mcServer.getPlayerList(); - playerList.load(entityPlayer); - entityPlayer.spawnIn(worldServer); - playerList.getPlayers().add(entityPlayer); - - // Get private playerByUUID Map from PlayerList class and add player to it - // private final Map playersByUUID = Maps.newHashMap(); - final Field playersByUUIDField = Reflector.getMapFieldWithTypes(PlayerList.class, UUID.class, ServerPlayer.class); - final Map playerByUUID = (Map) Reflector.getFieldValue(playersByUUIDField, playerList); - playerByUUID.put(uuid, entityPlayer); - - bukkitPlayer = Bukkit.getPlayer(uuid); - - if(bukkitPlayer == null) - throw new RuntimeException("Bukkit player with UUID " + uuid + " not found!"); - - final String joinMessage = "§e" + entityPlayer.displayName + " joined the game"; - final PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(bukkitPlayer, net.kyori.adventure.text.Component.text(joinMessage)); - Bukkit.getPluginManager().callEvent(playerJoinEvent); - - // Let other client know player is there - sendPacket(new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.ADD_PLAYER, entityPlayer)); - - worldServer.addNewPlayer(entityPlayer); - - // Send world info to player client - playerList.sendLevelInfo(entityPlayer, worldServer); - entityPlayer.sendServerStatus(playerList.getServer().getStatus()); - - // Spawn the player for the client - sendPacket(new ClientboundAddPlayerPacket(entityPlayer)); - - tickTaskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(OCMMain.getInstance(), entityPlayer::tick, 1, 1); - } - - public void removePlayer() { - if(tickTaskId != null) Bukkit.getScheduler().cancelTask(tickTaskId); - tickTaskId = null; - - final MinecraftServer mcServer = ((CraftServer) Bukkit.getServer()).getServer(); - - final net.kyori.adventure.text.Component quitMessage = net.kyori.adventure.text.Component.text("§e" + entityPlayer.displayName + " left the game"); - final PlayerQuitEvent playerQuitEvent = new PlayerQuitEvent(bukkitPlayer, quitMessage, PlayerQuitEvent.QuitReason.DISCONNECTED); - Bukkit.getPluginManager().callEvent(playerQuitEvent); - - entityPlayer.getBukkitEntity().disconnect(quitMessage.toString()); - - final PlayerList playerList = mcServer.getPlayerList(); - playerList.remove(entityPlayer); - } - - public void attack(org.bukkit.entity.Entity bukkitEntity) { - bukkitPlayer.attack(bukkitEntity); - } - - public void updateEquipment(EquipmentSlot slot, org.bukkit.inventory.ItemStack item) { - //todo try directly accessing the player inventory - // otherwise just set the attack value attribute instead - // could check Citizen's code to see how they give weapons - // might also just need to wait a tick - - //final ServerLevel worldServer = entityPlayer.x(); // entityPlayer.getWorld().getWorld().getHandle(); - // worldServer.broadcastEntityEvent(Entity, byte) - - //Defining the list of Pairs with EquipmentSlot and (NMS) ItemStack - final List> equipmentList = new ArrayList<>(); - equipmentList.add(new Pair<>(slot, CraftItemStack.asNMSCopy(item))); - - //Creating the packet - final ClientboundSetEquipmentPacket entityEquipment = new ClientboundSetEquipmentPacket(bukkitPlayer.getEntityId(), equipmentList); - sendPacket(entityEquipment); - // ((ServerLevel) this.level).getChunkSource().broadcast(this, new PacketPlayOutEntityEquipment(this.getId(), list)); - } - - private void sendPacket(Packet packet) { - Bukkit.getOnlinePlayers().stream() - .map(p -> ((CraftPlayer) p).getHandle().connection) - .forEach(connection -> connection.send(packet)); - } - - /** - * Make this player block with a shield - */ - public void doBlocking() { - bukkitPlayer.getInventory().setItemInMainHand(new org.bukkit.inventory.ItemStack(Material.SHIELD)); - - final LivingEntity entityLiving = ((CraftLivingEntity) bukkitPlayer).getHandle(); - entityLiving.startUsingItem(InteractionHand.MAIN_HAND); - // getUseDuration of SHIELD is 72000 - // For isBlocking to be true, useDuration - getUseItemRemainingTicks() must be >= 5 - // Which means we have to wait at least 5 ticks before user is actually blocking - // Here we just set it manually - final ReflectionRemapper reflectionRemapper = ReflectionRemapper.forReobfMappingsInPaperJar(); - final String remapped = reflectionRemapper.remapFieldName(LivingEntity.class, "useItemRemaining"); - final Field useItemRemainingField = Reflector.getField(LivingEntity.class, remapped); - Reflector.setFieldValue(useItemRemainingField, entityLiving, 200); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/InGameTester.java b/src/main/java/kernitus/plugin/OldCombatMechanics/tester/InGameTester.java deleted file mode 100644 index cf7ec8a6..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/InGameTester.java +++ /dev/null @@ -1,401 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.tester; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils; -import kernitus.plugin.OldCombatMechanics.utilities.damage.DefenceUtils; -import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages; -import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerData; -import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage; -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeInstance; -import org.bukkit.attribute.AttributeModifier; -import org.bukkit.command.CommandSender; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -import java.util.*; - -public class InGameTester { - - private final OCMMain ocm; - private Tally tally; - private CommandSender sender; - private Player attacker, defender; - private FakePlayer fakeAttacker, fakeDefender; - private final Queue testQueue; - - public InGameTester(OCMMain ocm) { - this.ocm = ocm; - testQueue = new ArrayDeque<>(); - } - - /** - * Perform all tests using the two specified players - */ - public void performTests(CommandSender sender, Location location) { - this.sender = sender; - fakeAttacker = new FakePlayer(); - fakeAttacker.spawn(location.add(2, 0, 0)); - fakeDefender = new FakePlayer(); - final Location defenderLocation = location.add(0, 0, 2); - fakeDefender.spawn(defenderLocation); - - attacker = Bukkit.getPlayer(fakeAttacker.getUuid()); - defender = Bukkit.getPlayer(fakeDefender.getUuid()); - - // Turn defender to face attacker - defenderLocation.setYaw(180); - defenderLocation.setPitch(0); - defender.teleport(defenderLocation); - - // modeset of attacker takes precedence - PlayerData playerData = PlayerStorage.getPlayerData(attacker.getUniqueId()); - playerData.setModesetForWorld(attacker.getWorld().getUID(), "old"); - PlayerStorage.setPlayerData(attacker.getUniqueId(), playerData); - - playerData = PlayerStorage.getPlayerData(defender.getUniqueId()); - playerData.setModesetForWorld(defender.getWorld().getUID(), "new"); - PlayerStorage.setPlayerData(defender.getUniqueId(), playerData); - - beforeAll(); - tally = new Tally(); - - // Queue all tests - //runAttacks(new ItemStack[]{}, () -> {}); // with no armour - testArmour(); - //testEnchantedMelee(new ItemStack[]{}, () -> {}); - - // Run all tests in the queue - runQueuedTests(); - } - - private void runAttacks(ItemStack[] armour, Runnable preparations) { - //testMelee(armour, preparations); - testEnchantedMelee(armour, preparations); - testOverdamage(armour, preparations); - } - - private void testArmour() { - final String[] materials = {"LEATHER", "CHAINMAIL", "GOLDEN", "IRON", "DIAMOND", "NETHERITE"}; - final String[] slots = {"BOOTS", "LEGGINGS", "CHESTPLATE", "HELMET"}; - final Random random = new Random(System.currentTimeMillis()); - - final ItemStack[] armourContents = new ItemStack[4]; - for (int i = 0; i < slots.length; i++) { - final String slot = slots[i]; - // Pick a random material for each slot - final String material = materials[random.nextInt(6)]; - - final ItemStack itemStack = new ItemStack(Material.valueOf(material + "_" + slot)); - - // Apply enchantment to armour piece - itemStack.addUnsafeEnchantment(Enchantment.PROTECTION_ENVIRONMENTAL, 50); - - armourContents[i] = itemStack; - } - - runAttacks(armourContents, () -> { - defender.getInventory().setArmorContents(armourContents); - // Test status effects on defence: resistance, fire resistance, absorption - defender.addPotionEffect(new PotionEffect(PotionEffectType.DAMAGE_RESISTANCE, 10, 1)); - fakeDefender.doBlocking(); - }); - } - - private void testEnchantedMelee(ItemStack[] armour, Runnable preparations) { - for (Material weaponType : WeaponDamages.getMaterialDamages().keySet()) { - final ItemStack weapon = new ItemStack(weaponType); - - // only axe and sword can have sharpness - try { - weapon.addEnchantment(Enchantment.DAMAGE_ALL, 3); - } catch (IllegalArgumentException ignored) { - } - - final String message = weaponType.name() + " Sharpness 3"; - queueAttack(new OCMTest(weapon, armour, 2, message, () -> { - preparations.run(); - defender.setMaximumNoDamageTicks(0); - attacker.addPotionEffect(new PotionEffect(PotionEffectType.INCREASE_DAMAGE, 10, 0, false)); - attacker.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS, 10, -1, false)); - Messenger.debug("TESTING WEAPON " + weaponType); - attacker.setFallDistance(2); // Crit - })); - } - } - - private void testMelee(ItemStack[] armour, Runnable preparations) { - for (Material weaponType : WeaponDamages.getMaterialDamages().keySet()) { - final ItemStack weapon = new ItemStack(weaponType); - queueAttack(new OCMTest(weapon, armour, 1, weaponType.name(), () -> { - preparations.run(); - defender.setMaximumNoDamageTicks(0); - })); - } - } - - private void testOverdamage(ItemStack[] armour, Runnable preparations) { - // 1, 5, 6, 7, 3, 8 according to OCM - // 1, 4, 5, 6, 2, 7 according to 1.9+ - Material[] weapons = {Material.WOODEN_HOE, Material.WOODEN_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.WOODEN_PICKAXE, Material.DIAMOND_SWORD}; - - for (Material weaponType : weapons) { - final ItemStack weapon = new ItemStack(weaponType); - queueAttack(new OCMTest(weapon, armour, 3, weaponType.name(), () -> { - preparations.run(); - defender.setMaximumNoDamageTicks(30); - })); - } - } - - private void queueAttack(OCMTest test) { - testQueue.add(test); - } - - private double calculateAttackDamage(ItemStack weapon) { - final Material weaponType = weapon.getType(); - // Attack components order: (Base + Potion effects, scaled by attack delay) + Critical Hit + (Enchantments, scaled by attack delay) - // Hurt components order: Overdamage - Armour Effects - double expectedDamage = WeaponDamages.getDamage(weaponType); - - // Weakness effect, 1.8: -0.5 - // We ignore the level as there is only one level of weakness potion - final double weaknessAddend = attacker.hasPotionEffect(PotionEffectType.WEAKNESS) ? -0.5 : 0; - - // Strength effect - // 1.8: +130% for each strength level - final PotionEffect strength = attacker.getPotionEffect(PotionEffectType.INCREASE_DAMAGE); - if (strength != null) - expectedDamage += (strength.getAmplifier() + 1) * 1.3 * expectedDamage; - - expectedDamage += weaknessAddend; - - // Take into account damage reduction because of cooldown - final float attackCooldown = defender.getAttackCooldown(); - expectedDamage *= 0.2F + attackCooldown * attackCooldown * 0.8F; - - // Critical hit - if (DamageUtils.isCriticalHit1_8(attacker)) { - expectedDamage *= 1.5; - } - - // Weapon Enchantments - double sharpnessDamage = DamageUtils.getOldSharpnessDamage(weapon.getEnchantmentLevel(Enchantment.DAMAGE_ALL)); - sharpnessDamage *= attackCooldown; // Scale by attack cooldown strength - expectedDamage += sharpnessDamage; - - return expectedDamage; - } - - private boolean wasFakeOverdamage(ItemStack weapon) { - double weaponDamage = calculateAttackDamage(weapon); - final double lastDamage = defender.getLastDamage(); - return (float) defender.getNoDamageTicks() > (float) defender.getMaximumNoDamageTicks() / 2.0F && - weaponDamage <= lastDamage; - } - - private boolean wasOverdamaged(double rawWeaponDamage) { - final double lastDamage = defender.getLastDamage(); - return (float) defender.getNoDamageTicks() > (float) defender.getMaximumNoDamageTicks() / 2.0F && - rawWeaponDamage > lastDamage; - } - - private float calculateExpectedDamage(ItemStack weapon, ItemStack[] armourContents) { - double expectedDamage = calculateAttackDamage(weapon); - - // Overdamage - if (wasOverdamaged(expectedDamage)) { - double lastDamage = defender.getLastDamage(); - Messenger.send(sender, "Overdamaged: " + expectedDamage + " - " + lastDamage + " = " + (expectedDamage - lastDamage)); - Messenger.debug("Overdamaged: " + expectedDamage + " - " + lastDamage + " = " + (expectedDamage - lastDamage)); - expectedDamage -= lastDamage; - } - - // BASE -> HARD_HAT -> BLOCKING -> ARMOUR -> RESISTANCE -> MAGIC -> ABSORPTION - - // Blocking - //1.8 default: (damage - 1) * 50% 1.9 default: 33% 1.11 default: 100% - if (defender.isBlocking()) { - Messenger.debug("DEFENDER IS BLOCKING " + expectedDamage); - //expectedDamage = (1.0F + expectedDamage) * 0.5F; - expectedDamage -= Math.max(0, (expectedDamage - 1)) * 0.5; - Messenger.debug("AFTER BLOCC " + expectedDamage); - } - - // Armour, resistance, armour enchants (1.8, with OldArmourStrength module) - expectedDamage = DefenceUtils.getDamageAfterArmour1_8(defender, expectedDamage, armourContents, EntityDamageEvent.DamageCause.ENTITY_ATTACK, false); - - /* 1.8 NMS - float f1 = f; - f = Math.max(f - this.getAbsorptionHearts(), 0.0F); - this.setAbsorptionHearts(this.getAbsorptionHearts() - (f1 - f)); - if (f != 0.0F) { - this.applyExhaustion(damagesource.getExhaustionCost()); - float f2 = this.getHealth(); - - this.setHealth(this.getHealth() - f); - this.bs().a(damagesource, f2, f); - if (f < 3.4028235E37F) { - this.a(StatisticList.x, Math.round(f * 10.0F)); - } - } - */ - - return (float) expectedDamage; - } - - private void runQueuedTests() { - Messenger.send(sender, "Running " + testQueue.size() + " tests"); - - // Listener gets called every time defender is damaged - Listener listener = new Listener() { - @EventHandler(priority = EventPriority.MONITOR) - public void onEvent(EntityDamageByEntityEvent e) { - final Entity damager = e.getDamager(); - if (damager.getUniqueId() != attacker.getUniqueId() || - e.getEntity().getUniqueId() != defender.getUniqueId()) return; - - final ItemStack weapon = ((Player) damager).getInventory().getItemInMainHand(); - final Material weaponType = weapon.getType(); - OCMTest test = testQueue.remove(); - ItemStack expectedWeapon = test.weapon; - float expectedDamage = calculateExpectedDamage(expectedWeapon, test.armour); - - while (weaponType != expectedWeapon.getType()) { // One of the attacks dealt no damage - expectedDamage = calculateExpectedDamage(expectedWeapon, test.armour); - Messenger.send(sender, "&bSKIPPED &f" + expectedWeapon.getType() + " &fExpected Damage: &b" + expectedDamage); - if (expectedDamage == 0) - tally.passed(); - else - tally.failed(); - test = testQueue.remove(); - expectedWeapon = test.weapon; - } - - - if (wasFakeOverdamage(weapon) && e.isCancelled()) { - Messenger.send(sender, "&aPASSED &fFake overdamage " + expectedDamage + " < " + ((LivingEntity) e.getEntity()).getLastDamage()); - tally.passed(); - } else { - final String weaponMessage = "E: " + expectedWeapon.getType().name() + " A: " + weaponType.name(); - TesterUtils.assertEquals(expectedDamage, (float) e.getFinalDamage(), tally, weaponMessage, sender); - } - } - }; - - Bukkit.getServer().getPluginManager().registerEvents(listener, ocm); - - final long testCount = testQueue.size(); - - // Cumulative attack delay for scheduling all the tests - long attackDelay = 0; - - for (OCMTest test : testQueue) { - attackDelay += test.attackDelay; - - Bukkit.getScheduler().runTaskLater(ocm, () -> { - beforeEach(); - test.preparations.run(); - preparePlayer(test.weapon); - attacker.attack(defender); - afterEach(); - }, attackDelay); - - } - - Bukkit.getScheduler().runTaskLater(ocm, () -> { - afterAll(testCount); - EntityDamageByEntityEvent.getHandlerList().unregister(listener); - }, attackDelay + 1); - } - - private void beforeAll() { - for (Player player : new Player[]{attacker, defender}) { - player.setGameMode(GameMode.SURVIVAL); - player.setMaximumNoDamageTicks(20); - player.setNoDamageTicks(0); // remove spawn invulnerability - player.setInvulnerable(false); - } - } - - private void afterAll(long testCount) { - fakeAttacker.removePlayer(); - fakeDefender.removePlayer(); - - final long missed = testCount - tally.getTotal(); - Messenger.sendNoPrefix(sender, "Passed: &a%d &rFailed: &c%d &rTotal: &7%d &rMissed: &7%d", tally.getPassed(), tally.getFailed(), tally.getTotal(), missed); - } - - private void beforeEach() { - for (Player player : new Player[]{attacker, defender}) { - player.getInventory().clear(); - player.setExhaustion(0); - player.setHealth(20); - } - } - - private void preparePlayer(ItemStack weapon) { - if (weapon.hasItemMeta()) { - final ItemMeta meta = weapon.getItemMeta(); - meta.addAttributeModifier(Attribute.GENERIC_ATTACK_SPEED, - new AttributeModifier(UUID.randomUUID(), "speed", 1000, - AttributeModifier.Operation.ADD_NUMBER, EquipmentSlot.HAND)); - weapon.setItemMeta(meta); - } - attacker.getInventory().setItemInMainHand(weapon); - attacker.updateInventory(); - - // Update attack attribute cause it won't get done with fake players - final AttributeInstance ai = attacker.getAttribute(Attribute.GENERIC_ATTACK_DAMAGE); - final AttributeInstance defenderArmour = defender.getAttribute(Attribute.GENERIC_ARMOR); - - weapon.getType().getDefaultAttributeModifiers(EquipmentSlot.HAND).get(Attribute.GENERIC_ATTACK_DAMAGE).forEach(am -> { - ai.removeModifier(am); - ai.addModifier(am); - }); - - // Update armour attribute - final ItemStack[] armourContents = defender.getInventory().getArmorContents(); - Messenger.debug("Armour: " + Arrays.stream(armourContents).filter(Objects::nonNull).map(is -> is.getType().name()).reduce((a, b) -> a + ", " + b).orElse("none")); - for (int i = 0; i < armourContents.length; i++) { - final ItemStack itemStack = armourContents[i]; - if (itemStack == null) continue; - final Material type = itemStack.getType(); - final EquipmentSlot slot = new EquipmentSlot[]{EquipmentSlot.FEET, EquipmentSlot.LEGS, EquipmentSlot.CHEST, EquipmentSlot.HEAD}[i]; - for (AttributeModifier attributeModifier : type.getDefaultAttributeModifiers(slot).get(Attribute.GENERIC_ARMOR)) { - defenderArmour.removeModifier(attributeModifier); - defenderArmour.addModifier(attributeModifier); - } - } - - } - - private void afterEach() { - for (Player player : new Player[]{attacker, defender}) { - player.setExhaustion(0); - player.setHealth(20); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/OCMTest.java b/src/main/java/kernitus/plugin/OldCombatMechanics/tester/OCMTest.java deleted file mode 100644 index 861bd140..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/OCMTest.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.tester; - -import org.bukkit.inventory.ItemStack; - -public class OCMTest { - final ItemStack weapon; - final long attackDelay; - final String message; - final Runnable preparations; - final ItemStack[] armour; - - public OCMTest(ItemStack weapon, ItemStack[] armour, long attackDelay, String message, Runnable preparations) { - this.weapon = weapon; - this.armour = armour; - this.attackDelay = attackDelay; - this.message = message; - this.preparations = preparations; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/Tally.java b/src/main/java/kernitus/plugin/OldCombatMechanics/tester/Tally.java deleted file mode 100644 index 8ac64241..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/Tally.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.tester; - -public class Tally { - private int passed = 0; - private int failed = 0; - - public void passed() { - passed++; - } - - public void failed() { - failed++; - } - - public int getPassed() { - return passed; - } - - public int getFailed() { - return failed; - } - - public int getTotal() { - return passed + failed; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/Config.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/Config.java deleted file mode 100644 index 09c6fecc..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/Config.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities; - -import kernitus.plugin.OldCombatMechanics.ModuleLoader; -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.module.OCMModule; -import kernitus.plugin.OldCombatMechanics.utilities.damage.EntityDamageByEntityListener; -import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; - -import javax.annotation.Nullable; -import java.io.InputStreamReader; -import java.util.*; -import java.util.logging.Level; -import java.util.stream.Collectors; - -public class Config { - - private static final String CONFIG_NAME = "config.yml"; - private static OCMMain plugin; - private static FileConfiguration config; - private static final Map> modesets = new HashMap<>(); - private static final Map> worlds = new HashMap<>(); - - public static void initialise(OCMMain plugin) { - Config.plugin = plugin; - config = plugin.getConfig(); - // Make sure to separately call reload() - } - - /** - * @return Whether config was changed or not - */ - private static boolean checkConfigVersion() { - final YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration( - new InputStreamReader(Objects.requireNonNull(plugin.getResource(CONFIG_NAME)))); - - if (config.getInt("config-version") != defaultConfig.getInt("config-version")) { - plugin.upgradeConfig(); - reload(); - return true; - } - - return false; - } - - - public static void reload() { - if (plugin.doesConfigExist()) { - plugin.reloadConfig(); - config = plugin.getConfig(); - } else - plugin.upgradeConfig(); - - // checkConfigVersion will call #reload() again anyways - if (checkConfigVersion()) return; - - Messenger.reloadConfig(config.getBoolean("debug.enabled"), config.getString("message-prefix")); - - WeaponDamages.initialise(plugin); //Reload weapon damages from config - - //Set EntityDamagedByEntityListener to enabled if either of these modules is enabled - final EntityDamageByEntityListener EDBEL = EntityDamageByEntityListener.getINSTANCE(); - if (EDBEL != null) { - EDBEL.setEnabled(moduleEnabled("old-tool-damage") || - moduleEnabled("old-potion-effects") - || moduleEnabled("old-critical-hits") - ); - } - - reloadModesets(); - reloadWorlds(); - - // Dynamically registers / unregisters all event listeners for optimal performance! - ModuleLoader.toggleModules(); - - ModuleLoader.getModules().forEach(module -> { - try { - module.reload(); - } catch (Exception e) { - plugin.getLogger() - .log(Level.WARNING, "Error reloading module '" + module.toString() + "'", e); - } - }); - - } - - private static void reloadModesets() { - modesets.clear(); - - final Set moduleNames = ModuleLoader.getModules().stream().map(OCMModule::getConfigName).collect(Collectors.toSet()); - final ConfigurationSection modesetsSection = config.getConfigurationSection("modesets"); - - // A set to keep track of all the modules that are already in a modeset - final Set modulesInModesets = new HashSet<>(); - - // Iterate over each modeset - for (String key : modesetsSection.getKeys(false)) { - // Retrieve the list of module names for the current modeset - final List moduleList = modesetsSection.getStringList(key); - final Set moduleSet = new HashSet<>(moduleList); - - // Add the current modeset and its modules to the map - modesets.put(key, moduleSet); - - // Add all modules in the current modeset to the tracking set - modulesInModesets.addAll(moduleSet); - } - - // Find modules not present in any modeset - final Set modulesNotInAnyModeset = new HashSet<>(moduleNames); - modulesNotInAnyModeset.removeAll(modulesInModesets); - - // Add any module not present in any modeset to all modesets - for (Set modeSet : modesets.values()) { - modeSet.addAll(modulesNotInAnyModeset); - } - } - - private static void reloadWorlds() { - worlds.clear(); - - final ConfigurationSection worldsSection = config.getConfigurationSection("worlds"); - - // Iterate over each world - for (String worldName : worldsSection.getKeys(false)) { - final World world = Bukkit.getWorld(worldName); - if(world == null){ - Messenger.warn("Configured world " + worldName + " not found, skipping (might be loaded later?)..."); - continue; - } - addWorld(world, worldsSection); - } - } - - public static void addWorld(World world){ - final ConfigurationSection worldsSection = config.getConfigurationSection("worlds"); - addWorld(world, worldsSection); - } - - public static void addWorld(World world, ConfigurationSection worldsSection) { - // Retrieve the list of modeset names for the current world - // Using a linkedhashset to remove duplicates but retain insertion order (important for default modeset) - final LinkedHashSet modesetsSet = new LinkedHashSet<>(worldsSection.getStringList(world.getName())); - - // Add the current world and its modesets to the map - worlds.put(world.getUID(), modesetsSet); - } - - public static void removeWorld(World world){ - worlds.remove(world.getUID()); - } - - /** - * Get the default modeset for the given world. - * @param worldId The UUID for the world to check the allowed modesets for - * @return The default modeset, if found. Otherwise null. - */ - public static @Nullable Set getDefaultModeset(UUID worldId){ - if(!worlds.containsKey(worldId)) return null; - - final Set set = worlds.get(worldId); - if(set == null || set.isEmpty()) return null; - - final Iterator iterator = set.iterator(); - if(iterator.hasNext()) { - final String modesetName = iterator.next(); - if(modesets.containsKey(modesetName)){ - return modesets.get(modesetName); - } - } - - return null; - } - - /** - * Checks whether the module is present in the default modeset for the specified world - * @param world The world to get the default modeset for - * @return Whether the module is enabled for the found modeset - */ - public static boolean moduleEnabled(String moduleName, World world) { - final ConfigurationSection section = config.getConfigurationSection(moduleName); - - if (section == null) { - plugin.getLogger().warning("Tried to check module '" + moduleName + "', but it didn't exist!"); - return false; - } - - if (!section.getBoolean("enabled")) return false; - if (world == null) return true; // Only checking if module is globally enabled - - final Set defaultModeset = getDefaultModeset(world.getUID()); - // If no default modeset found, the module should be enabled - if(defaultModeset == null){ - return true; - } - - // Check if module is in default modeset - return defaultModeset.contains(moduleName); - } - - /** - * Check if module is globally enable under its own config section - * @param moduleName The name of the module to check - * @return Whether the module has enabled: true in its config section - */ - public static boolean moduleEnabled(String moduleName) { - return moduleEnabled(moduleName, null); - } - - public static boolean debugEnabled() { - return moduleEnabled("debug", null); - } - - public static boolean moduleSettingEnabled(String moduleName, String moduleSettingName) { - return config.getBoolean(moduleName + "." + moduleSettingName); - } - - /** - * Only use if you can't access config through plugin instance - * - * @return config.yml instance - */ - public static FileConfiguration getConfig() { - return plugin.getConfig(); - } - - public static Map> getModesets(){ - return modesets; - } - - public static Map> getWorlds() { - return worlds; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/ConfigUtils.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/ConfigUtils.java deleted file mode 100644 index 152819c9..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/ConfigUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities; - -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionDurations; -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionTypeCompat; -import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; - -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * Various utilities for making it easier to work with {@link org.bukkit.configuration.Configuration Configurations}. - * - * @see org.bukkit.configuration.file.YamlConfiguration - * @see org.bukkit.configuration.ConfigurationSection - */ -public class ConfigUtils { - - /** - * Safely loads all doubles from a configuration section, reading both double and integer values. - * - * @param section The section from which to load the doubles. - * @return The map of doubles. - */ - public static Map loadDoubleMap(ConfigurationSection section) { - Objects.requireNonNull(section, "section cannot be null!"); - - return section.getKeys(false).stream() - .filter(((Predicate) section::isDouble).or(section::isInt)) - .collect(Collectors.toMap(key -> key, section::getDouble)); - } - - /** - * Loads the list of {@link Material Materials} with the given key from a configuration section. - * Safely ignores non-matching materials. - * - * @param section The section from which to load the material list. - * @param key The key of the material list. - * @return The loaded material list, or an empty list if there is no list at the given key. - */ - public static List loadMaterialList(ConfigurationSection section, String key) { - Objects.requireNonNull(section, "section cannot be null!"); - Objects.requireNonNull(key, "key cannot be null!"); - - if (!section.isList(key)) return new ArrayList<>(); - - return section.getStringList(key).stream() - .map(Material::matchMaterial) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Gets potion duration values from config for all configured potion types. - * Will create map of new potion type name to durations. - * - * @param section The section from which to load the duration values - * @return HashMap of {@link String} and {@link PotionDurations} - */ - public static HashMap loadPotionDurationsList(ConfigurationSection section) { - Objects.requireNonNull(section, "potion durations section cannot be null!"); - - final HashMap durationsHashMap = new HashMap<>(); - final ConfigurationSection durationsSection = section.getConfigurationSection("potion-durations"); - - final ConfigurationSection drinkableSection = durationsSection.getConfigurationSection("drinkable"); - final ConfigurationSection splashSection = durationsSection.getConfigurationSection("splash"); - - for (String newPotionTypeName : drinkableSection.getKeys(false)) { - try { - // Get durations in seconds and convert to ticks - final int drinkableDuration = drinkableSection.getInt(newPotionTypeName) * 20; - final int splashDuration = splashSection.getInt(newPotionTypeName) * 20; - final PotionTypeCompat potionTypeCompat = new PotionTypeCompat(newPotionTypeName); - - durationsHashMap.put(potionTypeCompat, new PotionDurations(drinkableDuration, splashDuration)); - } catch (IllegalArgumentException e) { - // In case the potion doesn't exist in the version running on the server - Messenger.debug("Skipping loading " + newPotionTypeName + " potion"); - } - } - - return durationsHashMap; - } - -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/EventRegistry.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/EventRegistry.java deleted file mode 100644 index 8626ea5f..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/EventRegistry.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities; - -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; -import org.bukkit.plugin.Plugin; - -import java.util.ArrayList; -import java.util.List; - -/** - * A simple utility class to ensure that a Listener is not registered more than once. - */ -public class EventRegistry { - private final Plugin plugin; - private final List listeners = new ArrayList<>(); - - public EventRegistry(Plugin plugin) { - this.plugin = plugin; - } - - /** - * Registers a listener and returns true if the listener was not already registered. - * - * @param listener The {@link Listener} to register. - * @return Whether the listener was successfully registered. - */ - public boolean registerListener(Listener listener) { - if (listeners.contains(listener)) return false; - - listeners.add(listener); - plugin.getServer().getPluginManager().registerEvents(listener, plugin); - return true; - } - - /** - * Unregisters a listener and returns true if the listener was already registered. - * - * @param listener The {@link Listener} to register. - * @return Whether the listener was successfully unregistered. - */ - public boolean unregisterListener(Listener listener) { - if (!listeners.contains(listener)) return false; - - listeners.remove(listener); - HandlerList.unregisterAll(listener); - return true; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/Messenger.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/Messenger.java deleted file mode 100644 index fa2f0ab6..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/Messenger.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; - -import java.util.Objects; -import java.util.logging.Level; - -public class Messenger { - - public static final String HORIZONTAL_BAR = ChatColor.STRIKETHROUGH + "----------------------------------------------------"; - private static OCMMain plugin; - - private static boolean DEBUG_ENABLED = false; - private static String PREFIX = "&6[OCM]&r"; - - public static void initialise(OCMMain plugin) { - Messenger.plugin = plugin; - } - - public static void reloadConfig(boolean debugEnabled, String prefix){ - DEBUG_ENABLED = debugEnabled; - PREFIX = prefix; - } - - public static void info(String message, Object... args) { - plugin.getLogger().info(TextUtils.stripColour(String.format(message, args))); - } - - public static void warn(Throwable e, String message, Object... args) { - plugin.getLogger().log(Level.WARNING, TextUtils.stripColour(String.format(message, args)), e); - } - - public static void warn(String message, Object... args) { - plugin.getLogger().log(Level.WARNING, TextUtils.stripColour(String.format(message, args))); - } - - /** - * This will format any ampersand (&) color codes, - * format any args passed to it using {@link String#format(String, Object...)}, - * and then send the message to the specified {@link CommandSender}. - * - * @param sender The {@link CommandSender} to send the message to. - * @param message The message to send. - * @param args The args to format the message with. - */ - public static void sendNoPrefix(CommandSender sender, String message, Object... args) { - Objects.requireNonNull(sender, "sender cannot be null!"); - Objects.requireNonNull(message, "message cannot be null!"); - // Prevents sending of individual empty messages, allowing for selective message disabling. - if (message.isEmpty()) return; - sender.sendMessage(TextUtils.colourise(String.format(message, args))); - } - - /** - * This will add the prefix to the message, format any ampersand (&) color codes, - * format any args passed to it using {@link String#format(String, Object...)}, - * and then send the message to the specified {@link CommandSender}. - * - * @param sender The {@link CommandSender} to send the message to. - * @param message The message to send. - * @param prefix The prefix to the message - * @param args The args to format the message with. - */ - private static void sendWithPrefix(CommandSender sender, String message, String prefix, Object... args) { - // Prevents sending of individual empty messages, allowing for selective message disabling. - if (message.isEmpty()) return; - sendNoPrefix(sender, prefix + " " + message, args); - } - - public static void send(CommandSender sender, String message, Object... args) { - sendWithPrefix(sender, message, PREFIX, args); - } - - private static void sendDebugMessage(CommandSender sender, String message, Object... args) { - sendWithPrefix(sender, message, "&1[Debug]&r", args); - } - - public static void debug(String message, Throwable throwable) { - if (DEBUG_ENABLED) plugin.getLogger().log(Level.INFO, message, throwable); - } - - public static void debug(String message, Object... args) { - if (DEBUG_ENABLED) info("[DEBUG] " + message, args); - } - - public static void debug(CommandSender sender, String message, Object... args) { - if (DEBUG_ENABLED) sendDebugMessage(sender, message, args); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/AttackCooldownTracker.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/AttackCooldownTracker.java deleted file mode 100644 index 26ac748c..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/AttackCooldownTracker.java +++ /dev/null @@ -1,57 +0,0 @@ -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.module.OCMModule; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.VersionCompatUtils; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.PlayerQuitEvent; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; -import java.util.UUID; -import java.util.WeakHashMap; - -/** - * Spigot versions below 1.16 did not have way of getting attack cooldown. - * Obtaining through NMS works, but value is reset before EntityDamageEvent is called. - * This means we must keep track of the cooldown to get the correct values. - */ -public class AttackCooldownTracker extends OCMModule { - private static AttackCooldownTracker INSTANCE; - private final Map lastCooldown; - - public AttackCooldownTracker(OCMMain plugin) { - super(plugin, "attack-cooldown-tracker"); - INSTANCE = this; - lastCooldown = new WeakHashMap<>(); - - Runnable cooldownTask = () -> Bukkit.getOnlinePlayers().forEach( - player -> lastCooldown.put(player.getUniqueId(), - VersionCompatUtils.getAttackCooldown(player) - )); - Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, cooldownTask, 0, 1L); - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent event){ - lastCooldown.remove(event.getPlayer().getUniqueId()); - } - - public static Float getLastCooldown(UUID uuid) { - return INSTANCE.lastCooldown.get(uuid); - } - - // Module is always enabled, because it will only be in list of modules if server - // itself requires it (i.e. is below 1.16 / does not have getAttackCooldown method) - @Override - public boolean isEnabled(@NotNull World world) { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/DamageUtils.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/DamageUtils.java deleted file mode 100644 index 0373aa6e..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/DamageUtils.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser; -import org.bukkit.Material; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -public class DamageUtils { - - // Method added in 1.16.4 - private static final SpigotFunctionChooser isInWater = SpigotFunctionChooser.apiCompatCall( - (le, params) -> le.isInWater(), - (le, params) -> le.getLocation().getBlock().getType() == Material.WATER - ); - - // Method added in 1.17.0 - private static final SpigotFunctionChooser isClimbing = SpigotFunctionChooser.apiCompatCall( - (le, params) -> le.isClimbing(), - (le, params) -> { - final Material material = le.getLocation().getBlock().getType(); - return material == Material.LADDER || material == Material.VINE; - } - ); - - // Method added in 1.16. Default parameter for reflected method is 0.5F - public static final SpigotFunctionChooser getAttackCooldown = - SpigotFunctionChooser.apiCompatCall( - (he, params) -> he.getAttackCooldown(), - (he, params) -> getAttackCooldown(he) - ); - - /** - * Gets last stored attack cooldown. Used when method is not available and we are keeping track of value ourselves. - * @param humanEntity The player to get the attack cooldown from - * @return The attack cooldown, as a value between 0 and 1 - */ - private static float getAttackCooldown(HumanEntity humanEntity){ - final Float cooldown = AttackCooldownTracker.getLastCooldown(humanEntity.getUniqueId()); - if(cooldown == null){ - Messenger.debug("Last attack cooldown null for " + humanEntity.getName() + ", assuming full attack strength"); - return 1L; - } - return cooldown; - } - - /** - * Get sharpness damage multiplier for 1.9 - * - * @param level The level of the enchantment - * @return Sharpness damage multiplier - */ - public static double getNewSharpnessDamage(int level) { - return level >= 1 ? 1 + (level - 1) * 0.5 : 0; - } - - /** - * Get sharpness damage multiplier for 1.8 - * - * @param level The level of the enchantment - * @return Sharpness damage multiplier - */ - public static double getOldSharpnessDamage(int level) { - return level >= 1 ? level * 1.25 : 0; - } - - /** - * Check preconditions for critical hit - * - * @param humanEntity Living entity to perform checks on - * @return Whether hit is critical - */ - public static boolean isCriticalHit1_8(HumanEntity humanEntity) { - /* Code from Bukkit 1.8_R3: - boolean flag = this.fallDistance > 0.0F && !this.onGround && !this.onClimbable() && !this.isInWater() - && !this.hasEffect(MobEffectList.BLINDNESS) && this.vehicle == null && entity instanceof EntityLiving; - */ - return humanEntity.getFallDistance() > 0.0F && - !humanEntity.isOnGround() && - !isClimbing.apply(humanEntity) && - !isInWater.apply(humanEntity) && - humanEntity.getActivePotionEffects().stream().map(PotionEffect::getType) - .noneMatch(e -> e == PotionEffectType.BLINDNESS) && - !humanEntity.isInsideVehicle(); - } - - public static boolean isCriticalHit1_9(Player player) { - return isCriticalHit1_8(player) && getAttackCooldown.apply(player) > 0.9F && !player.isSprinting(); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/DefenceUtils.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/DefenceUtils.java deleted file mode 100644 index 4318bc38..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/DefenceUtils.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser; -import kernitus.plugin.OldCombatMechanics.utilities.reflection.VersionCompatUtils; -import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat; -import org.bukkit.Material; -import org.bukkit.attribute.Attribute; -import org.bukkit.attribute.AttributeModifier; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.LivingEntity; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; - -import java.util.Collection; -import java.util.EnumSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.Supplier; - -import static kernitus.plugin.OldCombatMechanics.utilities.Messenger.debug; - -/** - * Utilities for calculating damage reduction from armour and status effects. - * Defence order is armour defence -> resistance -> armour enchants -> absorption - * BASE -> HARD_HAT -> BLOCKING -> ARMOUR -> RESISTANCE -> MAGIC -> ABSORPTION - * This class just deals with everything from armour onwards - */ -public class DefenceUtils { - private static final double REDUCTION_PER_ARMOUR_POINT = 0.04; - private static final double REDUCTION_PER_RESISTANCE_LEVEL = 0.2; - - private static final Set ARMOUR_IGNORING_CAUSES = EnumSet.of( - EntityDamageEvent.DamageCause.FIRE_TICK, - EntityDamageEvent.DamageCause.SUFFOCATION, - EntityDamageEvent.DamageCause.DROWNING, - EntityDamageEvent.DamageCause.STARVATION, - EntityDamageEvent.DamageCause.FALL, - EntityDamageEvent.DamageCause.VOID, - EntityDamageEvent.DamageCause.CUSTOM, - EntityDamageEvent.DamageCause.MAGIC, - EntityDamageEvent.DamageCause.WITHER, - // From 1.9 - EntityDamageEvent.DamageCause.FLY_INTO_WALL, - EntityDamageEvent.DamageCause.DRAGON_BREATH - // In 1.19 FIRE bypasses armour, but it doesn't in 1.8 so we don't add it here - ); - - // Stalagmite ignores armour but other blocks under CONTACT do not, explicitly checked below - static { - if (Reflector.versionIsNewerOrEqualTo(1, 11, 0)) - ARMOUR_IGNORING_CAUSES.add(EntityDamageEvent.DamageCause.CRAMMING); - if (Reflector.versionIsNewerOrEqualTo(1, 17, 0)) - ARMOUR_IGNORING_CAUSES.add(EntityDamageEvent.DamageCause.FREEZE); - } - - // Method added in 1.15 - private static final SpigotFunctionChooser getAbsorptionAmount = - SpigotFunctionChooser.apiCompatCall( - (le, params) -> le.getAbsorptionAmount(), - (le, params) -> Double.valueOf(VersionCompatUtils.getAbsorptionAmount(le))); - - /** - * Calculates the reduction by armour, resistance, armour enchantments and absorption. - * The values are updated directly in the map for each damage modifier. - * - * @param damagedEntity The entity that was damaged - * @param damageModifiers A map of the damage modifiers and their values from the event - * @param damageCause The cause of the damage - */ - @SuppressWarnings("deprecation") - public static void calculateDefenceDamageReduction(LivingEntity damagedEntity, - Map damageModifiers, - EntityDamageEvent.DamageCause damageCause, - boolean randomness) { - - final double armourPoints = damagedEntity.getAttribute(Attribute.GENERIC_ARMOR).getValue(); - // Make sure we don't go over 100% protection - final double armourReductionFactor = Math.min(1.0, armourPoints * REDUCTION_PER_ARMOUR_POINT); - - // applyArmorModifier() calculations from NMS - // Apply armour damage reduction after hard hat (wearing helmet & hit by block) and blocking reduction - double currentDamage = damageModifiers.get(EntityDamageEvent.DamageModifier.BASE) + - damageModifiers.getOrDefault(EntityDamageEvent.DamageModifier.HARD_HAT, 0.0) + - damageModifiers.getOrDefault(EntityDamageEvent.DamageModifier.BLOCKING, 0.0); - if (damageModifiers.containsKey(EntityDamageEvent.DamageModifier.ARMOR)) { - double armourReduction = 0; - // If the damage cause does not ignore armour - // If the block they are in is a stalagmite, also ignore armour - if (!ARMOUR_IGNORING_CAUSES.contains(damageCause) && - !(Reflector.versionIsNewerOrEqualTo(1, 19, 0) && - damageCause == EntityDamageEvent.DamageCause.CONTACT && - damagedEntity.getLocation().getBlock().getType() == Material.POINTED_DRIPSTONE) - ) { - armourReduction = currentDamage * -armourReductionFactor; - } - damageModifiers.put(EntityDamageEvent.DamageModifier.ARMOR, armourReduction); - currentDamage += armourReduction; - } - - // This is the applyMagicModifier() calculations from NMS - if (damageCause != EntityDamageEvent.DamageCause.STARVATION) { - // Apply resistance effect - if (damageModifiers.containsKey(EntityDamageEvent.DamageModifier.RESISTANCE) && - damageCause != EntityDamageEvent.DamageCause.VOID && - damagedEntity.hasPotionEffect(PotionEffectTypeCompat.RESISTANCE.get())) { - final int level = damagedEntity.getPotionEffect(PotionEffectTypeCompat.RESISTANCE.get()).getAmplifier() + 1; - // Make sure we don't go over 100% protection - final double resistanceReductionFactor = Math.min(1.0, level * REDUCTION_PER_RESISTANCE_LEVEL); - final double resistanceReduction = -resistanceReductionFactor * currentDamage; - damageModifiers.put(EntityDamageEvent.DamageModifier.RESISTANCE, resistanceReduction); - currentDamage += resistanceReduction; - } - - // Apply armour enchants - // Don't calculate enchants if damage already 0 (like 1.8 NMS). Enchants cap at 80% reduction - if (currentDamage > 0 && damageModifiers.containsKey(EntityDamageEvent.DamageModifier.MAGIC)) { - final double enchantsReductionFactor = calculateArmourEnchantmentReductionFactor(damagedEntity.getEquipment().getArmorContents(), damageCause, randomness); - final double enchantsReduction = currentDamage * -enchantsReductionFactor; - damageModifiers.put(EntityDamageEvent.DamageModifier.MAGIC, enchantsReduction); - currentDamage += enchantsReduction; - } - - // Absorption - if (damageModifiers.containsKey(EntityDamageEvent.DamageModifier.ABSORPTION)) { - final double absorptionAmount = getAbsorptionAmount.apply(damagedEntity); - double absorptionReduction = -Math.min(absorptionAmount, currentDamage); - damageModifiers.put(EntityDamageEvent.DamageModifier.ABSORPTION, absorptionReduction); - } - } - } - - /** - * Return the damage after applying armour, resistance, and armour enchants protections, following 1.8 algorithm. - * - * @param defender The entity that is being attacked - * @param baseDamage The base damage done by the event, including weapon enchants, potions, crits - * @param armourContents The 4 pieces of armour contained in the armour slots - * @param damageCause The source of damage - * @param randomness Whether to apply random multiplier - * @return The damage done to the entity after armour is taken into account - */ - public static double getDamageAfterArmour1_8(LivingEntity defender, double baseDamage, ItemStack[] armourContents, EntityDamageEvent.DamageCause damageCause, boolean randomness) { - double armourPoints = 0; - for (int i = 0; i < armourContents.length; i++) { - final ItemStack itemStack = armourContents[i]; - if (itemStack == null) continue; - final EquipmentSlot slot = new EquipmentSlot[]{EquipmentSlot.FEET, EquipmentSlot.LEGS, EquipmentSlot.CHEST, EquipmentSlot.HEAD}[i]; - armourPoints += getAttributeModifierSum(itemStack.getType().getDefaultAttributeModifiers(slot).get(Attribute.GENERIC_ARMOR)); - } - - final double reductionFactor = armourPoints * REDUCTION_PER_ARMOUR_POINT; - - // Apply armour damage reduction - double finalDamage = baseDamage - (ARMOUR_IGNORING_CAUSES.contains(damageCause) ? 0 : (baseDamage * reductionFactor)); - - // Calculate resistance - if (defender.hasPotionEffect(PotionEffectTypeCompat.RESISTANCE.get())) { - int resistanceLevel = defender.getPotionEffect(PotionEffectTypeCompat.RESISTANCE.get()).getAmplifier() + 1; - finalDamage *= 1.0 - (resistanceLevel * 0.2); - } - - // Don't calculate enchantment reduction if damage is already 0. NMS 1.8 does it this way. - final double enchantmentReductionFactor = calculateArmourEnchantmentReductionFactor(armourContents, damageCause, randomness); - if (finalDamage > 0) { - finalDamage -= finalDamage * enchantmentReductionFactor; - } - - debug("Reductions: Armour %.0f%%, Ench %.0f%%, Total %.2f%%, Start dmg: %.2f Final: %.2f", reductionFactor * 100, - enchantmentReductionFactor * 100, (reductionFactor + (1 - reductionFactor) * enchantmentReductionFactor) * 100, - baseDamage, finalDamage); - - return finalDamage; - } - - /** - * Applies all the operations for the attribute modifiers of a specific attribute. - * Does not take into account the base value. - */ - private static double getAttributeModifierSum(Collection modifiers) { - double sum = 0; - for (AttributeModifier modifier : modifiers) { - final double value = modifier.getAmount(); - switch (modifier.getOperation()) { - case ADD_SCALAR: - sum += Math.abs(value); - break; - case ADD_NUMBER: - sum += value; - break; - case MULTIPLY_SCALAR_1: - sum *= value; - break; - } - } - return sum; - } - - - private static double calculateArmourEnchantmentReductionFactor(ItemStack[] armourContents, EntityDamageEvent.DamageCause cause, boolean randomness) { - int totalEpf = 0; - for (ItemStack armourItem : armourContents) { - if (armourItem != null && armourItem.getType() != Material.AIR) { - for (EnchantmentType enchantmentType : EnchantmentType.values()) { - if (!enchantmentType.protectsAgainst(cause)) continue; - - int enchantmentLevel = armourItem.getEnchantmentLevel(enchantmentType.getEnchantment()); - - if (enchantmentLevel > 0) { - totalEpf += enchantmentType.getEpf(enchantmentLevel); - } - } - } - } - - // Cap at 25 - totalEpf = Math.min(25, totalEpf); - - // Multiply by random value between 50% and 100%, then round up - double multiplier = randomness ? ThreadLocalRandom.current().nextDouble(0.5, 1) : 1.0; - totalEpf = (int) Math.ceil(totalEpf * multiplier); - - // Cap at 20 - totalEpf = Math.min(20, totalEpf); - - return REDUCTION_PER_ARMOUR_POINT * totalEpf; - } - - private enum EnchantmentType { - // Data from https://minecraft.fandom.com/wiki/Armor#Mechanics - PROTECTION(() -> { - EnumSet damageCauses = EnumSet.of( - EntityDamageEvent.DamageCause.CONTACT, - EntityDamageEvent.DamageCause.ENTITY_ATTACK, - EntityDamageEvent.DamageCause.PROJECTILE, - EntityDamageEvent.DamageCause.FALL, - EntityDamageEvent.DamageCause.FIRE, - EntityDamageEvent.DamageCause.LAVA, - EntityDamageEvent.DamageCause.BLOCK_EXPLOSION, - EntityDamageEvent.DamageCause.ENTITY_EXPLOSION, - EntityDamageEvent.DamageCause.LIGHTNING, - EntityDamageEvent.DamageCause.POISON, - EntityDamageEvent.DamageCause.MAGIC, - EntityDamageEvent.DamageCause.WITHER, - EntityDamageEvent.DamageCause.FALLING_BLOCK, - EntityDamageEvent.DamageCause.THORNS, - EntityDamageEvent.DamageCause.DRAGON_BREATH - ); - if (Reflector.versionIsNewerOrEqualTo(1, 10, 0)) - damageCauses.add(EntityDamageEvent.DamageCause.HOT_FLOOR); - if (Reflector.versionIsNewerOrEqualTo(1, 12, 0)) - damageCauses.add(EntityDamageEvent.DamageCause.ENTITY_SWEEP_ATTACK); - - return damageCauses; - }, - 0.75, EnchantmentCompat.PROTECTION.get()), - FIRE_PROTECTION(() -> { - EnumSet damageCauses = EnumSet.of( - EntityDamageEvent.DamageCause.FIRE, - EntityDamageEvent.DamageCause.FIRE_TICK, - EntityDamageEvent.DamageCause.LAVA - ); - - if (Reflector.versionIsNewerOrEqualTo(1, 10, 0)) { - damageCauses.add(EntityDamageEvent.DamageCause.HOT_FLOOR); - } - - return damageCauses; - }, 1.25, EnchantmentCompat.FIRE_PROTECTION.get()), - BLAST_PROTECTION(() -> EnumSet.of( - EntityDamageEvent.DamageCause.ENTITY_EXPLOSION, - EntityDamageEvent.DamageCause.BLOCK_EXPLOSION - ), 1.5, EnchantmentCompat.BLAST_PROTECTION.get()), - PROJECTILE_PROTECTION(() -> EnumSet.of( - EntityDamageEvent.DamageCause.PROJECTILE - ), 1.5, EnchantmentCompat.PROJECTILE_PROTECTION.get()), - FALL_PROTECTION(() -> EnumSet.of( - EntityDamageEvent.DamageCause.FALL - ), 2.5, EnchantmentCompat.FEATHER_FALLING.get()); - - private final Set protection; - private final double typeModifier; - private final Enchantment enchantment; - - EnchantmentType(Supplier> protection, double typeModifier, Enchantment enchantment) { - this.protection = protection.get(); - this.typeModifier = typeModifier; - this.enchantment = enchantment; - } - - /** - * Returns whether the armour protects against the given damage cause. - * - * @param cause the damage cause - * @return true if the armour protects against the given damage cause - */ - public boolean protectsAgainst(EntityDamageEvent.DamageCause cause) { - return protection.contains(cause); - } - - /** - * Returns the bukkit enchantment. - * - * @return the bukkit enchantment - */ - public Enchantment getEnchantment() { - return enchantment; - } - - /** - * Returns the enchantment protection factor (EPF). - * - * @param level the level of the enchantment - * @return the EPF - */ - public int getEpf(int level) { - // floor ( (6 + level^2) * TypeModifier / 3 ) - return (int) Math.floor((6 + level * level) * typeModifier / 3); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/EntityDamageByEntityListener.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/EntityDamageByEntityListener.java deleted file mode 100644 index 54378cdf..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/EntityDamageByEntityListener.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.module.OCMModule; -import org.bukkit.Bukkit; -import org.bukkit.entity.Entity; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; - -import java.util.Map; -import java.util.UUID; -import java.util.WeakHashMap; - -public class EntityDamageByEntityListener extends OCMModule { - - private static EntityDamageByEntityListener INSTANCE; - private boolean enabled; - private final Map lastDamages; - - public EntityDamageByEntityListener(OCMMain plugin) { - super(plugin, "entity-damage-listener"); - INSTANCE = this; - lastDamages = new WeakHashMap<>(); - } - - public static EntityDamageByEntityListener getINSTANCE() { - return INSTANCE; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) - public void onEntityDamage(EntityDamageEvent event) { - final Entity damagee = event.getEntity(); - - if (!(event instanceof EntityDamageByEntityEvent)) { - // Damage immunity only applies to living entities - if (!(damagee instanceof LivingEntity)) return; - final LivingEntity livingDamagee = ((LivingEntity) damagee); - - restoreLastDamage(livingDamagee); - - double newDamage = event.getDamage(); // base damage, before defence calculations - - // Overdamage due to immunity - // Invulnerability will cause less damage if they attack with a stronger weapon while vulnerable - // That is, the difference in damage will be dealt, but only if new attack is stronger than previous one - checkOverdamage(livingDamagee, event, newDamage); - - if (newDamage < 0) { - debug("Damage was " + newDamage + " setting to 0"); - newDamage = 0; - } - - // Set damage, this should scale effects in the 1.9 way in case some of our modules are disabled - event.setDamage(newDamage); - debug("Attack damage (before defence): " + newDamage); - - } else { - final Entity damager = ((EntityDamageByEntityEvent) event).getDamager(); - - // Call event constructor before setting lastDamage back, because we need it for calculations - final OCMEntityDamageByEntityEvent e = new OCMEntityDamageByEntityEvent - (damager, damagee, event.getCause(), event.getDamage()); - - // Set last damage to actual value for other modules and plugins to use - // This will be set back to 0 in MONITOR listener on the next tick to detect all potential overdamages. - // If there is large delay between last time an entity was damaged and the next damage, - // the last damage might have been removed from the weak hash map. This is intended, as the immunity - // ticks tends to be a short period of time anyway and last damage is irrelevant after immunity has expired. - if (damagee instanceof LivingEntity) - restoreLastDamage((LivingEntity) damagee); - - // Call event for the other modules to make their modifications - plugin.getServer().getPluginManager().callEvent(e); - - if (e.isCancelled()) return; - - // Now we re-calculate damage modified by the modules and set it back to original event - // Attack components order: (Base + Potion effects, scaled by attack delay) + Critical Hit + (Enchantments, scaled by attack delay) - // Hurt components order: Overdamage - Armour - Resistance - Armour enchants - Absorption - double newDamage = e.getBaseDamage(); - - debug("Base: " + e.getBaseDamage(), damager); - debug("Base: " + e.getBaseDamage()); - - // Weakness potion - final double weaknessModifier = e.getWeaknessModifier() * e.getWeaknessLevel(); - final double weaknessAddend = e.isWeaknessModifierMultiplier() ? newDamage * weaknessModifier : weaknessModifier; - // Don't modify newDamage yet so both potion effects are calculated off of the base damage - debug("Weak: " + weaknessAddend); - debug("Weak: " + weaknessAddend, damager); - - // Strength potion - debug("Strength level: " + e.getStrengthLevel()); - debug("Strength level: " + e.getStrengthLevel(), damager); - double strengthModifier = e.getStrengthModifier() * e.getStrengthLevel(); - if (!e.isStrengthModifierMultiplier()) newDamage += strengthModifier; - else if (e.isStrengthModifierAddend()) newDamage *= ++strengthModifier; - else newDamage *= strengthModifier; - - debug("Strength: " + strengthModifier); - debug("Strength: " + strengthModifier, damager); - - newDamage += weaknessAddend; - - // Scale by attack delay - // float currentItemAttackStrengthDelay = 1.0D / GenericAttributes.ATTACK_SPEED * 20.0D - // attack strength ticker goes up by 1 every tick, is reset to 0 after an attack - // float f2 = MathHelper.clamp((attackStrengthTicker + 0.5) / currentItemAttackStrengthDelay, 0.0F, 1.0F); - // f *= 0.2F + f2 * f2 * 0.8F; - // the multiplier is equivalent to y = 0.8x^2 + 0.2 - // because x (f2) is always between 0 and 1, the multiplier will always be between 0.2 and 1 - // this implies 40 speed is the minimum to always have full attack strength - if (damager instanceof HumanEntity) { - final float cooldown = DamageUtils.getAttackCooldown.apply((HumanEntity) damager, 0.5F); // i.e. f2 - debug("Scale by attack delay: " + newDamage + " *= 0.2 + " + cooldown + "^2 * 0.8"); - newDamage *= 0.2F + cooldown * cooldown * 0.8F; - } - - // Critical hit - final double criticalMultiplier = e.getCriticalMultiplier(); - debug("Crit " + newDamage + " *= " + criticalMultiplier); - newDamage *= criticalMultiplier; - - // Enchantment damage, scaled by attack cooldown - double enchantmentDamage = e.getMobEnchantmentsDamage() + e.getSharpnessDamage(); - if (damager instanceof HumanEntity) { - final float cooldown = DamageUtils.getAttackCooldown.apply((HumanEntity) damager, 0.5F); - debug("Scale enchantments by attack delay: " + enchantmentDamage + " *= " + cooldown); - enchantmentDamage *= cooldown; - } - newDamage += enchantmentDamage; - debug("Mob " + e.getMobEnchantmentsDamage() + " Sharp: " + e.getSharpnessDamage() + " Scaled: " + enchantmentDamage, damager); - - if (damagee instanceof LivingEntity) { - // Overdamage due to immunity - // Invulnerability will cause less damage if they attack with a stronger weapon while vulnerable - // That is, the difference in damage will be dealt, but only if new attack is stronger than previous one - // Value before overdamage will become new "last damage" - newDamage = checkOverdamage(((LivingEntity) damagee), event, newDamage); - } - - if (newDamage < 0) { - debug("Damage was " + newDamage + " setting to 0", damager); - newDamage = 0; - } - - // Set damage, this should scale effects in the 1.9 way in case some of our modules are disabled - event.setDamage(newDamage); - debug("New Damage: " + newDamage, damager); - debug("Attack damage (before defence): " + newDamage); - } - } - - /** - * Set entity's last damage to 0 a tick after the event so all overdamage attacks get through. - * The last damage is overridden by NMS code regardless of what the actual damage is set to via Spigot. - * Finally, the LOWEST priority listener above will set the last damage back to the correct value - * for other plugins to use the next time the entity is damaged. - */ - @EventHandler(priority = EventPriority.MONITOR) - public void afterEntityDamage(EntityDamageEvent event) { - final Entity damagee = event.getEntity(); - - if (event instanceof EntityDamageByEntityEvent) { - if (lastDamages.containsKey(damagee.getUniqueId())) { - // Set last damage to 0, so we can detect attacks even by weapons with a weaker attack value than what OCM would calculate - Bukkit.getScheduler().runTaskLater(plugin, () -> { - ((LivingEntity) damagee).setLastDamage(0); - debug("Set last damage to 0", damagee); - debug("Set last damage to 0"); - }, 1L); - } - } else { - // if not EDBYE then we leave last damage as is - lastDamages.remove(damagee.getUniqueId()); - debug("Non-entity damage, using default last damage", damagee); - debug("Non-entity damage, using default last damage"); - } - } - - /** - * Restored the correct last damage for the given entity - * - * @param damagee The living entity to try to restore the last damage for - */ - private void restoreLastDamage(LivingEntity damagee) { - final Double lastStoredDamage = lastDamages.get(damagee.getUniqueId()); - if (lastStoredDamage != null) { - final LivingEntity livingDamagee = damagee; - livingDamagee.setLastDamage(lastStoredDamage); - debug("Set last damage back to " + lastStoredDamage, livingDamagee); - debug("Set last damage back to " + lastStoredDamage); - } - } - - private double checkOverdamage(LivingEntity livingDamagee, EntityDamageEvent event, double newDamage) { - final double newLastDamage = newDamage; - - if ((float) livingDamagee.getNoDamageTicks() > (float) livingDamagee.getMaximumNoDamageTicks() / 2.0F) { - // Last damage was either set to correct value above in this listener, or we're using the server's value - // If other plugins later modify BASE damage, they should either be taking last damage into account, - // or ignoring the event if it is cancelled - final double lastDamage = livingDamagee.getLastDamage(); - if (newDamage <= lastDamage) { - event.setDamage(0); - event.setCancelled(true); - debug("Was fake overdamage, cancelling " + newDamage + " <= " + lastDamage); - return 0; - } - - debug("Overdamage: " + newDamage + " - " + lastDamage); - // We must subtract previous damage from new weapon damage for this attack - newDamage -= livingDamagee.getLastDamage(); - - debug("Last damage " + lastDamage + " new damage: " + newLastDamage + " applied: " + newDamage - + " ticks: " + livingDamagee.getNoDamageTicks() + " /" + livingDamagee.getMaximumNoDamageTicks() - ); - } - // Update the last damage done, including when it was overdamage. - // This means attacks must keep increasing in value during immunity period to keep dealing overdamage. - lastDamages.put(livingDamagee.getUniqueId(), newLastDamage); - - return newDamage; - } - - -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/MobDamage.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/MobDamage.java deleted file mode 100644 index b0d1462e..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/MobDamage.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import com.google.common.collect.ImmutableMap; -import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.EntityType; -import org.bukkit.inventory.ItemStack; - -import java.util.Map; - -public class MobDamage { - - private static final Map enchants; - private static final Enchantment SMITE = EnchantmentCompat.SMITE.get(); - private static final Enchantment BANE_OF_ARTHROPODS = EnchantmentCompat.BANE_OF_ARTHROPODS.get(); - - static { - Map allMobs = ImmutableMap.builder() - // Undead (https://minecraft.gamepedia.com/Undead) - .put("SKELETON", SMITE) - .put("ZOMBIE", SMITE) - .put("WITHER", SMITE) - .put("WITHER_SKELETON", SMITE) - .put("ZOMBIFIED_PIGLIN", SMITE) - .put("SKELETON_HORSE", SMITE) - .put("STRAY", SMITE) - .put("HUSK", SMITE) - .put("PHANTOM", SMITE) - .put("DROWNED", SMITE) - .put("ZOGLIN", SMITE) - .put("ZOMBIE_HORSE", SMITE) - .put("ZOMBIE_VILLAGER", SMITE) - - // Arthropods (https://minecraft.gamepedia.com/Arthropod) - .put("SPIDER", BANE_OF_ARTHROPODS) - .put("CAVE_SPIDER", BANE_OF_ARTHROPODS) - .put("BEE", BANE_OF_ARTHROPODS) - .put("SILVERFISH", BANE_OF_ARTHROPODS) - .put("ENDERMITE", BANE_OF_ARTHROPODS) - - .build(); - - ImmutableMap.Builder enchantsBuilder = ImmutableMap.builder(); - - // Add these individually because some may not exist in the Minecraft version we're running - allMobs.keySet().forEach(entityName -> { - try { - final EntityType entityType = EntityType.valueOf(entityName); - final Enchantment enchantment = allMobs.get(entityName); - enchantsBuilder.put(entityType, enchantment); - } catch (IllegalArgumentException ignored) { - } // Mob not supported in this MC version - }); - enchants = enchantsBuilder.build(); - } - - /** - * Gets damage due to Smite and Bane of Arthropods enchantments, when applicable - * - * @param entity The type of entity that was attacked - * @param item The enchanted weapon used in the attack - * @return The damage due to the enchantments - */ - public static double getEntityEnchantmentsDamage(EntityType entity, ItemStack item) { - final Enchantment enchantment = enchants.get(entity); - - if (enchantment == null || enchantment != SMITE || enchantment != BANE_OF_ARTHROPODS) - return 0; - - return 2.5 * item.getEnchantmentLevel(enchantment); - } - -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/NewWeaponDamage.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/NewWeaponDamage.java deleted file mode 100644 index fb35a627..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/NewWeaponDamage.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import org.bukkit.Material; - -/** - * Default 1.9 Minecraft tool damage values - */ -public enum NewWeaponDamage { - - // common values - STONE_SWORD(5), STONE_SHOVEL(3.5F), STONE_PICKAXE(3), STONE_AXE(9), STONE_HOE(1), - IRON_SWORD(6), IRON_SHOVEL(4.5F), IRON_PICKAXE(4), IRON_AXE(9), IRON_HOE(1), - DIAMOND_SWORD(7), DIAMOND_SHOVEL(5.5F), DIAMOND_PICKAXE(5), DIAMOND_AXE(9), DIAMOND_HOE(1), - - // pre-1.13 values - STONE_SPADE(3.5F), IRON_SPADE(4.5F), DIAMOND_SPADE(5.5F), - WOOD_SWORD(4), WOOD_SPADE(2.5F), WOOD_PICKAXE(2), WOOD_AXE(7), WOOD_HOE(1), - GOLD_SWORD(4), GOLD_SPADE(2.5F), GOLD_PICKAXE(2), GOLD_AXE(7), GOLD_HOE(1), - - // post-1.13 values - WOODEN_SWORD(4), WOODEN_SHOVEL(2.5F), WOODEN_PICKAXE(2), WOODEN_AXE(7), WOODEN_HOE(1), - GOLDEN_SWORD(4), GOLDEN_SHOVEL(2.5F), GOLDEN_PICKAXE(2), GOLDEN_AXE(7), GOLDEN_HOE(1), - NETHERITE_SWORD(8), NETHERITE_SHOVEL(6.5F), NETHERITE_PICKAXE(6), NETHERITE_AXE(10), NETHERITE_HOE(1); - - private final float damage; - - NewWeaponDamage(float damage) { - this.damage = damage; - } - - public static float getDamage(String mat) { - return valueOf(mat).damage; - } - - public static float getDamage(Material mat) { - return getDamage(mat.toString()); - } - - public float getDamage() { - return damage; - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/OCMEntityDamageByEntityEvent.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/OCMEntityDamageByEntityEvent.java deleted file mode 100644 index be12fa94..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/OCMEntityDamageByEntityEvent.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat; -import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffects; -import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat; -import org.bukkit.Material; -import org.bukkit.entity.*; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.event.entity.EntityDamageEvent.DamageCause; -import org.bukkit.inventory.ItemStack; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -import java.util.Optional; - -import static kernitus.plugin.OldCombatMechanics.utilities.Messenger.debug; - -public class OCMEntityDamageByEntityEvent extends Event implements Cancellable { - - private boolean cancelled; - private static final HandlerList handlers = new HandlerList(); - - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } - - private final Entity damager, damagee; - private final DamageCause cause; - private double rawDamage; - - private ItemStack weapon; - private int sharpnessLevel; - private boolean hasWeakness; - // The levels as shown in-game, i.e. 1 or 2 corresponding to I and II - private int strengthLevel, weaknessLevel; - - private double baseDamage = 0, mobEnchantmentsDamage = 0, sharpnessDamage = 0, criticalMultiplier = 1; - private double strengthModifier = 0, weaknessModifier = 0; - - // In 1.9 strength modifier is an addend, in 1.8 it is a multiplier and addend (+130%) - private boolean isStrengthModifierMultiplier = false; - private boolean isStrengthModifierAddend = true; - private boolean isWeaknessModifierMultiplier = false; - - private boolean was1_8Crit = false; - private boolean wasSprinting = false; - - // Here we reverse-engineer all the various damages caused by removing them one at a time, backwards from what NMS code does. - // This is so the modules can listen to this event and make their modifications, then EntityDamageByEntityListener sets the new values back. - // Performs the opposite of the following: - // (Base + Potion effects, scaled by attack delay) + Critical Hit + (Enchantments, scaled by attack delay), Overdamage, Armour - public OCMEntityDamageByEntityEvent(Entity damager, Entity damagee, DamageCause cause, double rawDamage) { - this.damager = damager; - this.damagee = damagee; - this.cause = cause; - - // We ignore attacks like arrows etc. because we do not need to change the attack side of those - // Other modules such as old armour strength work independently of this event - if (!(damager instanceof LivingEntity)) { - setCancelled(true); - return; - } - - // The raw damage passed to this event is EDBE's BASE damage, which does not include armour effects or resistance etc (defence) - this.rawDamage = rawDamage; - - /* - Invulnerability will cause less damage if they attack with a stronger weapon while vulnerable. - We must detect this and account for it, instead of setting the usual base weapon damage. - We artificially set the last damage to 0 between events so that all hits will register, - however we only do this for DamageByEntity, so there could still be environmental damage (e.g. cactus). - */ - if (damagee instanceof LivingEntity) { - final LivingEntity livingDamagee = (LivingEntity) damagee; - if ((float) livingDamagee.getNoDamageTicks() > (float) livingDamagee.getMaximumNoDamageTicks() / 2.0F) { - // NMS code also checks if current damage is higher that previous damage. However, here the event - // already has the difference between the two as the raw damage, and the event does not fire at all - // if this precondition is not met. - - // Adjust for last damage being environmental sources (e.g. cactus, fall damage) - final double lastDamage = livingDamagee.getLastDamage(); - this.rawDamage = rawDamage + lastDamage; - - debug(livingDamagee, "Overdamaged!: " + livingDamagee.getNoDamageTicks() + "/" + - livingDamagee.getMaximumNoDamageTicks() + " last: " + livingDamagee.getLastDamage()); - } else { - debug(livingDamagee, "Invulnerability: " + livingDamagee.getNoDamageTicks() + "/" + - livingDamagee.getMaximumNoDamageTicks() + " last: " + livingDamagee.getLastDamage()); - } - } - - final LivingEntity livingDamager = (LivingEntity) damager; - - weapon = livingDamager.getEquipment().getItemInMainHand(); - // Yay paper. Why do you need to return null here? - if (weapon == null) weapon = new ItemStack(Material.AIR); - // Technically the weapon could be in the offhand, i.e. a bow. - // However, we are only concerned with melee weapons here, which will always be in the main hand. - - final EntityType damageeType = damagee.getType(); - - debug(livingDamager, "Raw attack damage: " + rawDamage); - debug(livingDamager, "Without overdamage: " + this.rawDamage); - - - mobEnchantmentsDamage = MobDamage.getEntityEnchantmentsDamage(damageeType, weapon); - sharpnessLevel = weapon.getEnchantmentLevel(EnchantmentCompat.SHARPNESS.get()); - sharpnessDamage = DamageUtils.getNewSharpnessDamage(sharpnessLevel); - - // Scale enchantment damage by attack cooldown - if (damager instanceof HumanEntity) { - final float cooldown = DamageUtils.getAttackCooldown.apply((HumanEntity) damager, 0.5F); - mobEnchantmentsDamage *= cooldown; - sharpnessDamage *= cooldown; - } - - debug(livingDamager, "Mob: " + mobEnchantmentsDamage + " Sharpness: " + sharpnessDamage); - - // Amount of damage including potion effects and critical hits - double tempDamage = this.rawDamage - mobEnchantmentsDamage - sharpnessDamage; - - debug(livingDamager, "No ench damage: " + tempDamage); - - // Check if it's a critical hit - if (livingDamager instanceof Player && DamageUtils.isCriticalHit1_8((HumanEntity) livingDamager)){ - was1_8Crit = true; - debug(livingDamager, "1.8 Critical hit detected"); - // In 1.9 a crit also requires the player not to be sprinting - if (DamageUtils.isCriticalHit1_9((Player) livingDamager)) { - debug(livingDamager, "1.9 Critical hit detected"); - debug("1.9 Critical hit detected"); - criticalMultiplier = 1.5; - tempDamage /= 1.5; - } - } - - // Un-scale the damage by the attack strength - if (damager instanceof HumanEntity) { - final float cooldown = DamageUtils.getAttackCooldown.apply((HumanEntity) damager, 0.5F); - tempDamage /= 0.2F + cooldown * cooldown * 0.8F; - } - - // amplifier 0 = Strength I amplifier 1 = Strength II - strengthLevel = PotionEffects.get(livingDamager, PotionEffectTypeCompat.STRENGTH.get()) - .map(PotionEffect::getAmplifier) - .orElse(-1) + 1; - - strengthModifier = strengthLevel * 3; - - debug(livingDamager, "Strength Modifier: " + strengthModifier); - - // Don't set has weakness if amplifier is > 0 or < -1, which is outside normal range and probably set by plugin - // We use an amplifier of -1 (Level 0) to have no effect so weaker attacks will register - final Optional weaknessAmplifier = PotionEffects.get(livingDamager, PotionEffectType.WEAKNESS).map(PotionEffect::getAmplifier); - hasWeakness = weaknessAmplifier.isPresent() && (weaknessAmplifier.get() == -1 || weaknessAmplifier.get() == 0); - weaknessLevel = weaknessAmplifier.orElse(-1) + 1; - - weaknessModifier = weaknessLevel * -4; - - debug(livingDamager, "Weakness Modifier: " + weaknessModifier); - - baseDamage = tempDamage + weaknessModifier - strengthModifier; - debug(livingDamager, "Base tool damage: " + baseDamage); - } - - public Entity getDamager() { - return damager; - } - - public Entity getDamagee() { - return damagee; - } - - public DamageCause getCause() { - return cause; - } - - public double getRawDamage() { - return rawDamage; - } - - public ItemStack getWeapon() { - return weapon; - } - - public int getSharpnessLevel() { - return sharpnessLevel; - } - - public double getStrengthModifier() { - return strengthModifier; - } - - public void setStrengthModifier(double strengthModifier) { - this.strengthModifier = strengthModifier; - } - - public int getStrengthLevel() { - return strengthLevel; - } - - /** - * Whether the attacker had the weakness potion effect, - * and the level of the effect was either 0 (used by OCM) or 1 (normal value). - * Values outside this range are to be ignored, as they are probably from other plugins. - */ - public boolean hasWeakness() { - return hasWeakness; - } - - public int getWeaknessLevel() { - return weaknessLevel; - } - - public double getWeaknessModifier() { - return weaknessModifier; - } - - public void setWeaknessModifier(double weaknessModifier) { - this.weaknessModifier = weaknessModifier; - } - - public void setWeaknessLevel(int weaknessLevel) { - this.weaknessLevel = weaknessLevel; - } - - public boolean isStrengthModifierMultiplier() { - return isStrengthModifierMultiplier; - } - - public void setIsStrengthModifierMultiplier(boolean isStrengthModifierMultiplier) { - this.isStrengthModifierMultiplier = isStrengthModifierMultiplier; - } - - public void setIsStrengthModifierAddend(boolean isStrengthModifierAddend) { - this.isStrengthModifierAddend = isStrengthModifierAddend; - } - - public boolean isWeaknessModifierMultiplier() { - return isWeaknessModifierMultiplier; - } - - public void setIsWeaknessModifierMultiplier(boolean weaknessModifierMultiplier) { - isWeaknessModifierMultiplier = weaknessModifierMultiplier; - } - - public boolean isStrengthModifierAddend() { - return isStrengthModifierAddend; - } - - public double getBaseDamage() { - return baseDamage; - } - - public void setBaseDamage(double baseDamage) { - this.baseDamage = baseDamage; - } - - public double getMobEnchantmentsDamage() { - return mobEnchantmentsDamage; - } - - public void setMobEnchantmentsDamage(double mobEnchantmentsDamage) { - this.mobEnchantmentsDamage = mobEnchantmentsDamage; - } - - public double getSharpnessDamage() { - return sharpnessDamage; - } - - public void setSharpnessDamage(double sharpnessDamage) { - this.sharpnessDamage = sharpnessDamage; - } - - public double getCriticalMultiplier() { - return criticalMultiplier; - } - - public void setCriticalMultiplier(double criticalMultiplier) { - this.criticalMultiplier = criticalMultiplier; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public void setCancelled(boolean cancelled) { - this.cancelled = cancelled; - } - - public boolean wasSprinting() { - return wasSprinting; - } - - public void setWasSprinting(boolean wasSprinting) { - this.wasSprinting = wasSprinting; - } - - public boolean was1_8Crit() { - return was1_8Crit; - } - - public void setWas1_8Crit(boolean was1_8Crit) { - this.was1_8Crit = was1_8Crit; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/WeaponDamages.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/WeaponDamages.java deleted file mode 100644 index bfb92c21..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/damage/WeaponDamages.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.damage; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils; -import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; - -import java.util.HashMap; -import java.util.Map; - -public class WeaponDamages { - - private static Map damages; - - private static OCMMain plugin; - - public static void initialise(OCMMain plugin) { - WeaponDamages.plugin = plugin; - reload(); - } - - private static void reload() { - final ConfigurationSection section = plugin.getConfig().getConfigurationSection("old-tool-damage.damages"); - damages = ConfigUtils.loadDoubleMap(section); - } - - public static double getDamage(Material mat) { - final String name = mat.name().replace("GOLDEN", "GOLD").replace("WOODEN", "WOOD").replace("SHOVEL", "SPADE"); - return damages.getOrDefault(name, -1.0); - } - - public static Map getMaterialDamages() { - final Map materialMap = new HashMap<>(); - damages.forEach((name, damage) -> { - final String newName = name.replace("GOLD", "GOLDEN").replace("WOOD", "WOODEN").replace("SPADE", "SHOVEL"); - materialMap.put(Material.valueOf(newName), damage); - }); - return materialMap; - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffectTypeCompat.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffectTypeCompat.java deleted file mode 100644 index 06e66cab..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffectTypeCompat.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.potions; - -import org.bukkit.potion.PotionEffectType; -import org.jetbrains.annotations.Nullable; - -import java.util.Locale; - -public enum PotionEffectTypeCompat { - RESISTANCE("DAMAGE_RESISTANCE"), - NAUSEA("CONFUSION"), - HASTE("FAST_DIGGING"), - INSTANT_DAMAGE("HARM"), - INSTANT_HEALTH("HEAL"), - STRENGTH("INCREASE_DAMAGE"), - JUMP_BOOST("JUMP"), - SLOWNESS("SLOW"), - MINING_FATIGUE("SLOW_DIGGING"), - ; - - private PotionEffectType potionEffectType; - - PotionEffectTypeCompat(String oldName) { - // Try loading the new name first - // This only happens once per enum name - potionEffectType = PotionEffectType.getByName(name()); - // If the new name doesn't exist, fall back to the old name - if (potionEffectType == null) { - potionEffectType = PotionEffectType.getByName(oldName); - } - - if (potionEffectType == null) { - throw new IllegalStateException("PotionEffectType not found for: " + name() + " or " + oldName); - } - } - - public PotionEffectType get() { - return potionEffectType; - } - - /** - * Gets correct PotionEffectType for currently-running server version given new name. - * @param newName The PotionEffectType >=1.20.6 name - * @return The PotionEffectType for the currently-running server version, or null if not found. - */ - public static @Nullable PotionEffectType fromNewName(String newName) { - try { - // See if new name needs mapping to old - return valueOf(newName.toUpperCase(Locale.ROOT)).get(); - } catch (IllegalArgumentException e){ - // Otherwise use new name directly - return PotionEffectType.getByName(newName); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffects.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffects.java deleted file mode 100644 index a780f451..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffects.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.potions; - -import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser; -import org.bukkit.entity.LivingEntity; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -import java.util.Optional; - -public class PotionEffects { - - private static final SpigotFunctionChooser getPotionEffectsFunction = - SpigotFunctionChooser.apiCompatCall((le, type) -> le.getPotionEffect(type), - (le, type) -> - le.getActivePotionEffects().stream() - .filter(potionEffect -> potionEffect.getType().equals(type)) - .findAny() - .orElse(null) - ); - - /** - * Returns the {@link PotionEffect} of a given {@link PotionEffectType} for a given {@link LivingEntity}, if present. - * - * @param entity the entity to query - * @param type the type to search - * @return the {@link PotionEffect} if present - */ - public static Optional get(LivingEntity entity, PotionEffectType type) { - return Optional.ofNullable(getOrNull(entity, type)); - } - - /** - * Returns the {@link PotionEffect} of a given {@link PotionEffectType} for a given {@link LivingEntity}, if present. - * - * @param entity the entity to query - * @param type the type to search - * @return the {@link PotionEffect} or null if not present - */ - public static PotionEffect getOrNull(LivingEntity entity, PotionEffectType type) { - return getPotionEffectsFunction.apply(entity, type); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionTypeCompat.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionTypeCompat.java deleted file mode 100644 index 339de19f..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionTypeCompat.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.potions; - -import org.bukkit.inventory.meta.PotionMeta; -import org.bukkit.potion.PotionType; -import org.jetbrains.annotations.Nullable; - -import java.util.Locale; - -public class PotionTypeCompat { - /** - * Map new potion type names to old (pre 1.20.5) - */ - private enum PotionTypeMapper { - HARMING("INSTANT_DAMAGE"), - STRONG_HARMING("INSTANT_DAMAGE"), - HEALING("INSTANT_HEAL"), - STRONG_HEALING("INSTANT_HEAL"), - LEAPING("JUMP"), - STRONG_LEAPING("JUMP"), - LONG_LEAPING("JUMP"), - REGENERATION("REGEN"), - STRONG_REGENERATION("REGEN"), - LONG_REGENERATION("REGEN"), - SWIFTNESS("SPEED"), - STRONG_SWIFTNESS("SPEED"), - LONG_SWIFTNESS("SPEED"); - - private final String oldName; - - PotionTypeMapper(String oldName) { - this.oldName = oldName; - } - } - - private final String oldName; - private final String newName; - private final PotionType potionType; - private final boolean isStrong; - private final boolean isLong; - - public PotionType getType() { - return potionType; - } - - public boolean isStrong() { - return isStrong; - } - - public boolean isLong() { - return isLong; - } - - public String getNewName() { - return newName; - } - - @Override - public int hashCode() { - return newName.hashCode(); - } - - @Override - public boolean equals(Object o) { - return o instanceof PotionTypeCompat && newName.equals(((PotionTypeCompat) o).newName); - } - - /** - * Get PotionType for currently-running server. Requires newName and oldName to already be set. - * - * @return PotionType if found. - * @throws IllegalArgumentException If potion type could not be found. - */ - private @Nullable PotionType getPotionType() { - - PotionType potionType; - try { - potionType = PotionType.valueOf(this.newName); - } catch (IllegalArgumentException e) { - - // If running >=1.20.5, UNCRAFTABLE has been turned into null type - if(this.oldName.equals("UNCRAFTABLE")) return null; - - try { - potionType = PotionType.valueOf(this.oldName); - } catch (IllegalArgumentException exception) { - throw new IllegalArgumentException("Invalid potion type, tried " + newName + " and " + oldName, exception); - } - } - return potionType; - } - - /** - * Create an instance of {@link PotionTypeCompat} starting from the new potion name. (>=1.20.5) - * Name will be turned into upper case as needed. - * - * @param newName The new potion type name, e.g. strong_leaping. - */ - public PotionTypeCompat(String newName) { - this.newName = newName.toUpperCase(Locale.ROOT); - - String oldName; - try { // See if it's one of the potion names that changed - oldName = PotionTypeMapper.valueOf(newName).oldName; - } catch (IllegalArgumentException e) { - // Name did not change, but remove modifiers - oldName = this.newName.replace("STRONG_", "").replace("LONG_", ""); - } - this.oldName = oldName; - - this.potionType = getPotionType(); - - isStrong = newName.startsWith("STRONG_"); - isLong = newName.startsWith("LONG_"); - } - - - /** - * Create an instance of {@link PotionTypeCompat} starting from the old potion name. (<1.20.5) - * Name will be turned into upper case as needed. - * - * @param oldName The old potion type name, e.g. jump. - * @param isStrong Whether the potion is upgraded to amplifier 1 (II). - * @param isLong Whether the potion is extended. - */ - public PotionTypeCompat(String oldName, boolean isStrong, boolean isLong) { - this.oldName = oldName.toUpperCase(Locale.ROOT); - - String newName = null; - for (PotionTypeMapper mapped : PotionTypeMapper.values()) { - // Check if the old name matches - if (mapped.oldName.equals(this.oldName)) { - // Check the 'strong' and 'long' flags - final String mappedName = mapped.name(); - if (isStrong == mappedName.startsWith("STRONG_") && isLong == mappedName.startsWith("LONG_")) { - newName = mappedName; - break; - } - } - } - - if (newName == null) { // Name did not change - if (isStrong) - this.newName = "STRONG_" + this.oldName; - else if (isLong) - this.newName = "LONG_" + this.oldName; - else - this.newName = this.oldName; - } else this.newName = newName; - - this.potionType = getPotionType(); - this.isStrong = isStrong; - this.isLong = isLong; - } - - - /** - * Create an instance of {@link PotionTypeCompat} based on methods available in currently-running server version. - * @return Instance of {@link PotionTypeCompat}, if found. - */ - public static PotionTypeCompat fromPotionMeta(PotionMeta potionMeta) { - try { // For >=1.20.5 - final PotionType potionType = potionMeta.getBasePotionType(); - return new PotionTypeCompat(potionType.name()); - } catch (NoSuchMethodError e) { - final var potionData = potionMeta.getBasePotionData(); - final PotionType potionType = potionData.getType(); - return new PotionTypeCompat(potionType.name(), potionData.isUpgraded(), potionData.isExtended()); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/Reflector.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/Reflector.java deleted file mode 100644 index 06d7624d..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/Reflector.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.reflection; - -import kernitus.plugin.OldCombatMechanics.utilities.reflection.type.ClassType; -import org.bukkit.Bukkit; - -import java.lang.reflect.*; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class Reflector { - private static String version; - private static int majorVersion, minorVersion, patchVersion; - - static { - try { - // Split on the "-" to just get the version information - version = Bukkit.getServer().getBukkitVersion().split("-")[0]; - final String[] splitVersion = version.split("\\."); - - majorVersion = Integer.parseInt(splitVersion[0]); - minorVersion = Integer.parseInt(splitVersion[1]); - if(splitVersion.length > 2) { - patchVersion = Integer.parseInt(splitVersion[2]); - } else { - patchVersion = 0; - } - } catch (Exception e) { - System.err.println("Failed to load Reflector: " + e.getMessage()); - } - } - - public static String getVersion() { - return version; - } - - /** - * Checks if the current server version is newer or equal to the one provided. - * - * @param major the target major version - * @param minor the target minor version. 0 for all - * @param patch the target patch version. 0 for all - * @return true if the server version is newer or equal to the one provided - */ - public static boolean versionIsNewerOrEqualTo(int major, int minor, int patch) { - if (getMajorVersion() < major) return false; - if (getMinorVersion() < minor) return false; - return getPatchVersion() >= patch; - } - - private static int getMajorVersion() { - return majorVersion; - } - - private static int getMinorVersion() { - return minorVersion; - } - - private static int getPatchVersion() { - return patchVersion; - } - - public static Class getClass(ClassType type, String name) { - return getClass(type.qualifyClassName(name)); - } - - public static Class getClass(String fqn) { - try { - return Class.forName(fqn); - } catch (ClassNotFoundException e) { - throw new RuntimeException("Couldn't load class " + fqn, e); - } - } - - public static Method getMethod(Class clazz, String name) { - return Arrays.stream(clazz.getMethods()) - .filter(method -> method.getName().equals(name)) - .findFirst() - .orElse(null); - } - - public static Method getMethod(Class clazz, String name, int parameterCount) { - return Arrays.stream(clazz.getMethods()) - .filter(method -> method.getName().equals(name) && method.getParameterCount() == parameterCount) - .findFirst() - .orElse(null); - } - - public static Method getMethod(Class clazz, Class returnType, String... parameterTypeSimpleNames){ - List typeNames = Arrays.asList(parameterTypeSimpleNames); - return Arrays.stream(clazz.getMethods()) - .filter(method -> method.getReturnType() == returnType) - .filter(it -> getParameterNames.apply(it).equals(typeNames)) - .findFirst() - .orElse(null); - } - - private static final Function> getParameterNames = method -> Arrays - .stream(method.getParameters()) - .map(Parameter::getType) - .map(Class::getSimpleName) - .collect(Collectors.toList()); - - public static Method getMethod(Class clazz, String name, String... parameterTypeSimpleNames) { - List typeNames = Arrays.asList(parameterTypeSimpleNames); - return Stream.concat( - Arrays.stream(clazz.getDeclaredMethods()), - Arrays.stream(clazz.getMethods()) - ) - .filter(it -> it.getName().equals(name)) - .filter(it -> getParameterNames.apply(it).equals(typeNames)) - .peek(it -> it.setAccessible(true)) - .findFirst() - .orElse(null); - } - - public static Method getMethodByGenericReturnType(TypeVariable typeVar, Class clazz){ - for (Method method : clazz.getMethods()){ - if (method.getGenericReturnType().getTypeName().equals(typeVar.getName())){ - return method; - } - } - throw new RuntimeException("Method with type " + typeVar + " not found"); - } - - - public static T invokeMethod(Method method, Object handle, Object... params) { - try { - @SuppressWarnings("unchecked") - T t = (T) method.invoke(handle, params); - return t; - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - /** - * Resolves the given method, caches it and then uses that instance for all future invocations. - *

- * The returned function just invokes the cached method for a given target. - * - * @param clazz the clazz the method is in - * @param name the name of the method - * @param the type of the handle - * @param the type of the parameter(s) - * @param the type of the method result - * @return a function that invokes the retrieved cached method for its argument - */ - public static BiFunction memoiseMethodInvocation(Class clazz, String name, String... argTypes) { - final Method method = getMethod(clazz, name, argTypes); - return (t, u) -> { - // If they did not want to send any arguments, should be zero-length array - // This check is necessary cause of varargs, otherwise we get 1 length array of 0-length array - if(u instanceof Object[] && ((Object[]) u).length == 0) - return invokeMethod(method, t); - - return invokeMethod(method, t, u); - }; - } - - public static Field getField(Class clazz, String fieldName) { - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return field; - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - - public static Field getFieldByType(Class clazz, String simpleClassName) { - for (Field declaredField : clazz.getDeclaredFields()) { - if (declaredField.getType().getSimpleName().equals(simpleClassName)) { - declaredField.setAccessible(true); - return declaredField; - } - } - throw new RuntimeException("Field with type " + simpleClassName + " not found"); - } - - public static Field getMapFieldWithTypes(Class clazz, Class keyType, Class valueType) { - for (Field field : clazz.getDeclaredFields()) { - // Check if the field is a Map - if (Map.class.isAssignableFrom(field.getType())) { - // Get the generic type of the field - final Type genericType = field.getGenericType(); - if (genericType instanceof ParameterizedType) { - final ParameterizedType parameterizedType = (ParameterizedType) genericType; - final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); - // Check if the map's key and value types match the specified classes - if (actualTypeArguments.length == 2 && - actualTypeArguments[0].equals(keyType) && - actualTypeArguments[1].equals(valueType)) { - field.setAccessible(true); - return field; - } - } - } - } - throw new RuntimeException("Map field with key type " + keyType.getSimpleName() + - " and value type " + valueType.getSimpleName() + " not found"); - } - - public static Object getFieldValueByType(Object object, String simpleClassName) throws Exception { - Stream publicFields = Stream.of(object.getClass().getFields()); - Stream declaredFields = Stream.of(object.getClass().getDeclaredFields()); - Stream allFields = Stream.concat(publicFields, declaredFields); - - // Find the first field that matches the type name - Field matchingField = allFields - .filter(declaredField -> declaredField.getType().getSimpleName().equals(simpleClassName)) - .findFirst() - .orElseThrow(() -> new NoSuchFieldException("Couldn't find field with type " + simpleClassName + " in " + object.getClass())); - - // Make the field accessible and return its value - matchingField.setAccessible(true); - return matchingField.get(object); - } - - public static Object getFieldValue(Field field, Object handle) { - field.setAccessible(true); - try { - return field.get(handle); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - public static void setFieldValue(Field field, Object handle, Object value) { - field.setAccessible(true); - try { - field.set(handle, value); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - public static Constructor getConstructor(Class clazz, int numParams) { - return Stream.concat( - Arrays.stream(clazz.getDeclaredConstructors()), - Arrays.stream(clazz.getConstructors()) - ) - .filter(constructor -> constructor.getParameterCount() == numParams) - .peek(it -> it.setAccessible(true)) - .findFirst() - .orElse(null); - } - - public static Constructor getConstructor(Class clazz, String... parameterTypeSimpleNames) { - Function, List> getParameterNames = constructor -> Arrays - .stream(constructor.getParameters()) - .map(Parameter::getType) - .map(Class::getSimpleName) - .collect(Collectors.toList()); - List typeNames = Arrays.asList(parameterTypeSimpleNames); - return Stream.concat( - Arrays.stream(clazz.getDeclaredConstructors()), - Arrays.stream(clazz.getConstructors()) - ) - .filter(constructor -> getParameterNames.apply(constructor).equals(typeNames)) - .peek(it -> it.setAccessible(true)) - .findFirst() - .orElse(null); - } - - /** - * Checks if a given class somehow inherits from another class - * - * @param toCheck The class to check - * @param inheritedClass The inherited class, it should have - * @return True if {@code toCheck} somehow inherits from - * {@code inheritedClass} - */ - public static boolean inheritsFrom(Class toCheck, Class inheritedClass) { - if (inheritedClass.isAssignableFrom(toCheck)) { - return true; - } - - for (Class implementedInterface : toCheck.getInterfaces()) { - if (inheritsFrom(implementedInterface, inheritedClass)) { - return true; - } - } - - return false; - } - - public static T getUnchecked(UncheckedReflectionSupplier supplier) { - try { - return supplier.get(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - - public static void doUnchecked(UncheckedReflectionRunnable runnable) { - try { - runnable.run(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - - public interface UncheckedReflectionSupplier { - T get() throws ReflectiveOperationException; - } - - public interface UncheckedReflectionRunnable { - void run() throws ReflectiveOperationException; - } - -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/SpigotFunctionChooser.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/SpigotFunctionChooser.java deleted file mode 100644 index 403fdc4d..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/SpigotFunctionChooser.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.utilities.reflection; - -import java.util.function.BiFunction; - -/** - * Chooses a Spigot API function to use - * Chooses a function to apply based on a test supplier, remembers the choice and only uses the corresponding - * function in the future. - *

- * The branch to pick is determined during the first execution of its {@link #apply(Object, Object)} method!. - * This means that no matter how often the feature branch is invoked, it will never reconsider its choice. - * - * @param the type of the entity to apply the function to - * @param the type of the extra parameter(s). Use {@link Object} if unused, or list of objects if multiple. - * @param the return type of the function - */ -public class SpigotFunctionChooser { - - private final BiFunction test; - private final BiFunction trueBranch; - private final BiFunction falseBranch; - private BiFunction chosen; - - /** - * Creates a new {@link SpigotFunctionChooser}, which chooses between two given functions. - * - * @param test the test supplier that will be invoked to choose a branch - * @param trueBranch the branch to pick when then test is true - * @param falseBranch the branch to pick when then test is false - */ - public SpigotFunctionChooser(BiFunction test, BiFunction trueBranch, BiFunction falseBranch) { - this.test = test; - this.trueBranch = trueBranch; - this.falseBranch = falseBranch; - } - - /** - * Applies the stored action to the given target and chooses what branch to use on the first call. - * - * @param target the target to apply it to - * @param parameters the extra parameters to pass to the function - * @return the result of applying the function to the given target - */ - public R apply(T target, U parameters) { - if (chosen == null) { - synchronized (this) { - if (chosen == null) { - chosen = test.apply(target, parameters) ? trueBranch : falseBranch; - } - } - } - return chosen.apply(target, parameters); - } - - /** - * Version without extra parameter(s) of {@link #apply(Object, Object)} - */ - @SuppressWarnings("unchecked") - public R apply(T target) { - return apply(target, (U) new Object[0]); - } - - /** - * Creates a {@link SpigotFunctionChooser} that uses the success parameter when the action completes without an - * exception and otherwise uses the failure parameter. - *

- * The action is, per the doc for {@link SpigotFunctionChooser} only called once. - * - * @param action the action to invoke - * @param success the branch to take when no exception occurs - * @param failure the branch to take when an exception occurs - * @param the type of the class containing the method - * @param the type of the parameter(s) - * @param the return type of the method - * @return a {@link SpigotFunctionChooser} that picks the branch based on whether action threw an exception - */ - private static SpigotFunctionChooser onException(ExceptionalFunction action, - BiFunction success, - BiFunction failure) { - return new SpigotFunctionChooser<>( - (t, u) -> { - try { - action.apply(t, u); - return true; - } catch (ExceptionalFunction.WrappedException e) { - return false; - } - }, - success, failure - ); - } - - /** - * Calls the Spigot API method if possible, otherwise uses reflection to access same method. - * Useful for API methods that were only added after a certain version. Caches chosen method for performance. - * - *

Note: 1.16 is last version with Spigot-mapped fields, 1.17 with Spigot-mapped methods. - * - * @param apiCall A reference to the function that should be called - * @param clazz The class containing the function to be accessed via reflection - * @param name The name of the function to be accessed via reflection - * @return A new instance of {@link SpigotFunctionChooser} - */ - public static SpigotFunctionChooser apiCompatReflectionCall(ExceptionalFunction apiCall, - Class clazz, String name, - String... argTypes) { - return onException(apiCall, apiCall, Reflector.memoiseMethodInvocation(clazz, name, argTypes)); - } - - /** - * Calls the Spigot API method if possible, otherwise uses the provided function as a workaround. - *

- * This should be used to avoid reflection wherever possible, making the plugin more compatible. - * Chosen method is cached for performance. Do not use method references as they are eagerly-bound in Java 8. - *

- * - * @param apiCall A reference to the function that should be called - * @param altFunc A function that should instead be called if API method not available. - * @return A new instance of {@link SpigotFunctionChooser} - */ - public static SpigotFunctionChooser apiCompatCall(ExceptionalFunction apiCall, BiFunction altFunc) { - return onException(apiCall, apiCall, altFunc); - } - - @FunctionalInterface - public interface ExceptionalFunction extends BiFunction { - // TODO might want to only check for NoSuchMethodException, - // cause what if the method is there and the exception is for some other reason? - - /** - * Called by {@link #apply(Object, Object)}, this method is the target of the functional interface and where you can - * write your logic, that might throw an exception. - * - * @param t the function argument - * @return the function result - */ - R applyWithException(T t, U params) throws Throwable; - - /** - * {@inheritDoc} - * - * @param t {@inheritDoc} - * @return {@inheritDoc} - * @throws WrappedException if any *Throwable* is thrown - */ - @Override - default R apply(T t, U u) { - try { - return applyWithException(t, u); - } catch (Throwable e) { - throw new WrappedException(e); - } - } - - class WrappedException extends RuntimeException { - WrappedException(Throwable cause) { - super(cause); - } - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/VersionCompatUtils.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/VersionCompatUtils.java deleted file mode 100644 index d8858bce..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/VersionCompatUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.reflection; - -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.LivingEntity; - -import java.lang.reflect.Method; -import java.util.Map; -import java.util.WeakHashMap; - -/** - * Utilities to help with keeping compatibility across multiple versions of the game. - */ -public class VersionCompatUtils { - private static Method cooldownMethod; - private static final Map, Method> absorptionAmountMethodCache = new WeakHashMap<>(); - - /** - * Returns a Craft object from the given Spigot object, e.g. CraftPlayer from Player. - * Useful for accessing properties not available through Spigot's API. - * - * @param spigotObject The spigot object to get the handle of, e.g. Player - * @return The Craft object - */ - private static Object getCraftHandle(Object spigotObject) { - return Reflector.invokeMethod(Reflector.getMethod(spigotObject.getClass(), "getHandle"), spigotObject); - } - - public static float getAttackCooldown(HumanEntity he) { - final Object craftHumanEntity = getCraftHandle(he); - // public float x(float a), grab by return and param type, cause name changes in each version - if (cooldownMethod == null) // cache this to not search for it every single time - cooldownMethod = Reflector.getMethod(getCraftHandle(he).getClass(), float.class, "float"); - return Reflector.invokeMethod(cooldownMethod, craftHumanEntity, 0.5F); - } - - public static float getAbsorptionAmount(LivingEntity livingEntity) { - final Object craftLivingEntity = getCraftHandle(livingEntity); - final Class leClass = craftLivingEntity.getClass(); - final Method absorptionAmountMethod; - // Cache method for each subclass of LivingEntity to not search for it every single time - // Cannot cache for LE itself because method is obtained from each subclass - if(!absorptionAmountMethodCache.containsKey(leClass)){ - absorptionAmountMethod = Reflector.getMethod(craftLivingEntity.getClass(), "getAbsorptionHearts"); - absorptionAmountMethodCache.put(leClass, absorptionAmountMethod); - } else { - absorptionAmountMethod = absorptionAmountMethodCache.get(leClass); - } - - // Give useful debugging information in case the method cannot be applied - if (!absorptionAmountMethod.getDeclaringClass().isAssignableFrom(craftLivingEntity.getClass())) { - throw new IllegalArgumentException( - "Cannot call method '" + absorptionAmountMethod + "' of class '" + absorptionAmountMethod.getDeclaringClass().getName() - + "' using object '" + craftLivingEntity + "' of class '" + craftLivingEntity.getClass().getName() + "' because" - + " object '" + craftLivingEntity + "' is not an instance of '" + absorptionAmountMethod.getDeclaringClass().getName() + "'"); - } - - return Reflector.invokeMethod(absorptionAmountMethod, craftLivingEntity); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/ModesetListener.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/ModesetListener.java deleted file mode 100644 index 1c86da15..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/ModesetListener.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.storage; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import kernitus.plugin.OldCombatMechanics.module.OCMModule; -import kernitus.plugin.OldCombatMechanics.utilities.Config; -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.world.WorldLoadEvent; -import org.bukkit.event.world.WorldUnloadEvent; - -import java.util.Set; -import java.util.UUID; - -/** - * Listens to players changing world / spawning etc. - * and updates modeset accordingly - */ -public class ModesetListener extends OCMModule { - - public ModesetListener(OCMMain plugin) { - super(plugin, "modeset-listener"); - } - - @Override - public boolean isEnabled() { - return true; - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { - final Player player = event.getPlayer(); - final UUID playerId = player.getUniqueId(); - final PlayerData playerData = PlayerStorage.getPlayerData(playerId); - final String modesetFromName = playerData.getModesetForWorld(event.getFrom().getUID()); - updateModeset(player, player.getWorld().getUID(), modesetFromName); - } - - private static void updateModeset(Player player, UUID worldId, String modesetFromName) { - final UUID playerId = player.getUniqueId(); - final PlayerData playerData = PlayerStorage.getPlayerData(playerId); - final String originalModeset = playerData.getModesetForWorld(worldId); - String modesetName = playerData.getModesetForWorld(worldId); - - // Get modesets allowed in to world - Set allowedModesets = Config.getWorlds().get(worldId); - if (allowedModesets == null || allowedModesets.isEmpty()) - allowedModesets = Config.getModesets().keySet(); - - // If they don't have a modeset in toWorld yet - if (modesetName == null) { - // Try to use modeset of world they are coming from - if (modesetFromName != null && allowedModesets.contains(modesetFromName)) - modesetName = modesetFromName; - else // Otherwise, if the from modeset is not allowed, use default for to world - modesetName = allowedModesets.stream().findFirst().orElse(null); - } - - // If the modeset changed, set and save - if (originalModeset == null || !originalModeset.equals(modesetName)) { - playerData.setModesetForWorld(worldId, modesetName); - PlayerStorage.setPlayerData(playerId, playerData); - PlayerStorage.scheduleSave(); - - Messenger.send(player, - Config.getConfig().getString("mode-messages.mode-set", - "&4ERROR: &rmode-messages.mode-set string missing"), - modesetName - ); - } - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerJoin(PlayerJoinEvent event) { - final Player player = event.getPlayer(); - updateModeset(player, player.getWorld().getUID(), null); - } - - @EventHandler(ignoreCancelled = false) - public void onWorldLoad(WorldLoadEvent event) { - final World world = event.getWorld(); - Config.addWorld(world); - Messenger.info("Loaded configured world " + world.getName()); - } - - @EventHandler(ignoreCancelled = false) - public void onWorldUnload(WorldUnloadEvent event) { - final World world = event.getWorld(); - Config.removeWorld(world); - Messenger.info("Unloaded configured world " + world.getName()); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerData.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerData.java deleted file mode 100644 index c27aac82..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerData.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.storage; - -import org.bson.Document; -import org.jetbrains.annotations.Nullable; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class PlayerData { - private Map modesetByWorld; - - public PlayerData() { - modesetByWorld = new HashMap<>(); - } - - public Map getModesetByWorld() { - return modesetByWorld; - } - - public void setModesetByWorld(Map modesetByWorld) { - this.modesetByWorld = modesetByWorld; - } - - public void setModesetForWorld(UUID worldId, String modeset) { - modesetByWorld.put(worldId, modeset); - } - - public @Nullable String getModesetForWorld(UUID worldId) { - return modesetByWorld.get(worldId); - } - - public static PlayerData fromDocument(Document doc) { - final PlayerData playerData = new PlayerData(); - final Document modesetByWorldDoc = (Document) doc.get("modesetByWorld"); - if (modesetByWorldDoc != null) { - for (Map.Entry entry : modesetByWorldDoc.entrySet()) { - UUID worldId = UUID.fromString(entry.getKey()); - String modeset = (String) entry.getValue(); - playerData.setModesetForWorld(worldId, modeset); - } - } - return playerData; - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerDataCodec.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerDataCodec.java deleted file mode 100644 index d1a504bd..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerDataCodec.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.storage; - -import org.bson.BsonReader; -import org.bson.BsonWriter; -import org.bson.Document; -import org.bson.codecs.Codec; -import org.bson.codecs.DecoderContext; -import org.bson.codecs.DocumentCodec; -import org.bson.codecs.EncoderContext; - -import java.util.Map; -import java.util.UUID; - -public class PlayerDataCodec implements Codec { - - @Override - public void encode(BsonWriter writer, PlayerData value, EncoderContext encoderContext) { - final Document document = new Document(); - Document modesetByWorldDoc = new Document(); - for (Map.Entry entry : value.getModesetByWorld().entrySet()) { - modesetByWorldDoc.put(entry.getKey().toString(), entry.getValue()); - } - document.put("modesetByWorld", modesetByWorldDoc); - new DocumentCodec().encode(writer, document, encoderContext); - } - - @Override - public Class getEncoderClass() { - return PlayerData.class; - } - - @Override - public PlayerData decode(BsonReader reader, DecoderContext decoderContext) { - final Document document = new DocumentCodec().decode(reader, decoderContext); - return PlayerData.fromDocument(document); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerStorage.java b/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerStorage.java deleted file mode 100644 index 4c1a3edb..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerStorage.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.utilities.storage; - -import kernitus.plugin.OldCombatMechanics.OCMMain; -import org.bson.*; -import org.bson.codecs.*; -import org.bson.codecs.configuration.CodecRegistries; -import org.bson.codecs.configuration.CodecRegistry; -import org.bson.io.BasicOutputBuffer; -import org.bukkit.Bukkit; -import org.bukkit.scheduler.BukkitTask; - -import java.io.File; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; - -/** - * Stores data associated with players to disk, persisting across server restarts. - */ -public class PlayerStorage { - - private static OCMMain plugin; - private static Path dataFilePath; - private static DocumentCodec documentCodec; - private static Document data; - private static final AtomicReference saveTask = new AtomicReference<>(); - private static CodecRegistry codecRegistry; - - public static void initialise(OCMMain plugin) { - PlayerStorage.plugin = plugin; - dataFilePath = Paths.get(plugin.getDataFolder() + File.separator + "players.bson"); - - codecRegistry = CodecRegistries.fromRegistries( - CodecRegistries.fromCodecs(new DocumentCodec()), // Explicitly provide a DocumentCodec - CodecRegistries.fromCodecs(new PlayerDataCodec()), - CodecRegistries.fromProviders(new BsonValueCodecProvider(), new ValueCodecProvider()) // For BSON values - ); - - PlayerStorage.documentCodec = new DocumentCodec(codecRegistry); - - data = loadData(); - - saveTask.set(null); - } - - private static Document loadData() { - if (Files.notExists(dataFilePath)) return new Document(); - - try { - byte[] data = Files.readAllBytes(dataFilePath); - final BsonReader reader = new BsonBinaryReader(ByteBuffer.wrap(data)); - return documentCodec.decode(reader, DecoderContext.builder().build()); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "Error loading player data", e); - } - return new Document(); - } - - public static void scheduleSave() { - // Schedule a task for later, if there isn't one already scheduled - saveTask.compareAndSet(null, - Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> { - instantSave(); - saveTask.set(null); - }, 2400L) // Save after 2 minutes - ); - } - - public static void instantSave() { - final BasicOutputBuffer outputBuffer = new BasicOutputBuffer(); - final BsonWriter writer = new BsonBinaryWriter(outputBuffer); - documentCodec.encode(writer, data, EncoderContext.builder().isEncodingCollectibleDocument(true).build()); - writer.flush(); - - try { - Files.write(dataFilePath, outputBuffer.toByteArray()); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "Error saving player data", e); - } finally { - outputBuffer.close(); - } - } - - public static PlayerData getPlayerData(UUID uuid) { - final Document playerDoc = (Document) data.get(uuid.toString()); - if (playerDoc == null) { - final PlayerData playerData = new PlayerData(); - setPlayerData(uuid, playerData); - scheduleSave(); - return playerData; - } - final BsonDocument bsonDocument = new BsonDocumentWrapper<>(playerDoc, documentCodec); - return codecRegistry.get(PlayerData.class).decode(bsonDocument.asBsonReader(), DecoderContext.builder().build()); - } - - public static void setPlayerData(UUID uuid, PlayerData playerData) { - // Create a BsonDocumentWriter to hold the encoded data - BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument()); - - // Get the PlayerDataCodec from the CodecRegistry - PlayerDataCodec playerDataCodec = (PlayerDataCodec) codecRegistry.get(PlayerData.class); - - // Encode the PlayerData object to the writer - playerDataCodec.encode(writer, playerData, EncoderContext.builder().isEncodingCollectibleDocument(true).build()); - - // Retrieve the BsonDocument - BsonDocument bsonDocument = writer.getDocument(); - - // Convert the BsonDocument to a Document - Document document = new Document(); - bsonDocument.forEach((key, value) -> document.put(key, value.isDocument() ? new Document(value.asDocument()) : value)); - - // Put the Document into your data map - data.put(uuid.toString(), document); - } -} \ No newline at end of file diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/enchantments/EnchantmentCompat.java b/src/main/java/kernitus/plugin/OldCombatMechanics/versions/enchantments/EnchantmentCompat.java deleted file mode 100644 index d5a0b848..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/enchantments/EnchantmentCompat.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package kernitus.plugin.OldCombatMechanics.versions.enchantments; - -import org.bukkit.enchantments.Enchantment; -import org.jetbrains.annotations.Nullable; - -import java.util.Locale; - -public enum EnchantmentCompat { - UNBREAKING("DURABILITY"), - PROTECTION("PROTECTION_ENVIRONMENTAL"), - FIRE_PROTECTION("PROTECTION_FIRE"), - BLAST_PROTECTION("PROTECTION_EXPLOSIONS"), - PROJECTILE_PROTECTION("PROTECTION_PROJECTILE"), - FEATHER_FALLING("PROTECTION_FALL"), - SMITE("DAMAGE_UNDEAD"), - BANE_OF_ARTHROPODS("DAMAGE_ARTHROPODS"), - SHARPNESS("DAMAGE_ALL"); - - private Enchantment enchantment; - - EnchantmentCompat(String oldName) { - // Try loading the new name first - // This only happens once per enum name - enchantment = Enchantment.getByName(name()); - // If the new name doesn't exist, fall back to the old name - if (enchantment == null) { - enchantment = Enchantment.getByName(oldName); - } - - if (enchantment == null) { - throw new IllegalStateException("PotionEffectType not found for: " + name() + " or " + oldName); - } - } - - public Enchantment get() { - return enchantment; - } - - /** - * Gets correct {@link Enchantment} for currently-running server version given new name. - * - * @param newName The {@link Enchantment} >=1.20.6 name - * @return The {@link Enchantment} for the currently-running server version, or null if not found. - */ - public static @Nullable Enchantment fromNewName(String newName) { - try { - // See if new name needs mapping to old - return valueOf(newName.toUpperCase(Locale.ROOT)).get(); - } catch (IllegalArgumentException e) { - // Otherwise use new name directly - return Enchantment.getByName(newName); - } - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/DualVersionedMaterial.java b/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/DualVersionedMaterial.java deleted file mode 100644 index 243ed493..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/DualVersionedMaterial.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.versions.materials; - -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -import java.util.function.Supplier; - -/** - * A material that has a version for before 1.13 and after 1.13. - */ -public class DualVersionedMaterial implements VersionedMaterial { - - private Supplier oldItem; - private Supplier newItem; - - /** - * Creates a new dual versioned material. - * - * @param oldItem the item supplier for the old version - * @param newItem the item supplier for the new version - */ - public DualVersionedMaterial(Supplier oldItem, Supplier newItem) { - this.oldItem = oldItem; - this.newItem = newItem; - } - - @Override - public ItemStack newInstance() { - return getItemSupplier().get(); - } - - @Override - @SuppressWarnings("deprecation") - public boolean isSame(ItemStack other) { - ItemStack baseInstance = newInstance(); - - // items do not differ in more than those two things - return baseInstance.getType() == other.getType() && baseInstance.getDurability() == other.getDurability(); - } - - private Supplier getItemSupplier() { - return Reflector.versionIsNewerOrEqualTo(1, 13, 0) ? newItem : oldItem; - } - - @Override - public String toString() { - return "DualVersionedMaterial{" + - "picked=" + (getItemSupplier() == newItem ? "new" : "old") - + ", item=" + newInstance() - + '}'; - } - - /** - * Returns a new {@link DualVersionedMaterial} based on the material names. - * - * @param nameOld the old name - * @param nameNew the new name - * @return a dual versioned material using the supplied names - */ - public static VersionedMaterial ofMaterialNames(String nameOld, String nameNew) { - return new DualVersionedMaterial(fromMaterial(nameOld), fromMaterial(nameNew)); - } - - private static Supplier fromMaterial(String name) { - return () -> new ItemStack(Material.matchMaterial(name)); - } -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/MaterialRegistry.java b/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/MaterialRegistry.java deleted file mode 100644 index b56702c2..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/MaterialRegistry.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.versions.materials; - -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -/** - * Contains Materials that are different in {@literal > 1.13} and {@literal < 1.13}. - */ -public class MaterialRegistry { - - public static VersionedMaterial LAPIS_LAZULI = new DualVersionedMaterial( - () -> new ItemStack(Material.matchMaterial("INK_SACK"), 1, (short) 4), - () -> new ItemStack(Material.matchMaterial("LAPIS_LAZULI")) - ); - - public static VersionedMaterial ENCHANTED_GOLDEN_APPLE = new DualVersionedMaterial( - () -> new ItemStack(Material.GOLDEN_APPLE, 1, (short) 1), - () -> new ItemStack(Material.valueOf("ENCHANTED_GOLDEN_APPLE")) - ); -} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/NameListVersionedMaterial.java b/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/NameListVersionedMaterial.java deleted file mode 100644 index 2bb647b9..00000000 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/NameListVersionedMaterial.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package kernitus.plugin.OldCombatMechanics.versions.materials; - -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -/** - * A material that tries each name in a given list until it finds a working material. - */ -public class NameListVersionedMaterial implements VersionedMaterial { - - private Material finalMaterial; - - private NameListVersionedMaterial(Material finalMaterial) { - this.finalMaterial = finalMaterial; - } - - @Override - public ItemStack newInstance() { - return new ItemStack(finalMaterial); - } - - @Override - public boolean isSame(ItemStack other) { - return other.getType() == finalMaterial; - } - - /** - * Returns a new {@link VersionedMaterial} that picks the first working one from a list of names. - * - * @param names the names of the materials - * @return the versioned material - * @throws IllegalArgumentException if no material was valid - */ - public static VersionedMaterial ofNames(String... names) { - for (String name : names) { - Material material = Material.matchMaterial(name); - if (material != null) { - return new NameListVersionedMaterial(material); - } - - material = Material.matchMaterial(name, true); - if (material != null) { - return new NameListVersionedMaterial(material); - } - } - - throw new IllegalArgumentException("Could not find any working material, tried: " + String.join(",", names) + "."); - } -} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/ModuleLoader.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/ModuleLoader.kt new file mode 100644 index 00000000..4377dc6d --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/ModuleLoader.kt @@ -0,0 +1,36 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics + +import kernitus.plugin.OldCombatMechanics.module.OCMModule +import kernitus.plugin.OldCombatMechanics.utilities.EventRegistry +import kernitus.plugin.OldCombatMechanics.utilities.Messenger + +object ModuleLoader { + private lateinit var eventRegistry: EventRegistry + val modules: MutableList = ArrayList() + + fun addModule(module: OCMModule) = modules.add(module) + + fun initialise(plugin: OCMMain) { + eventRegistry = EventRegistry(plugin) + } + + fun toggleModules() = modules.forEach { setState(it, it.isEnabled()) } + + private fun setState(module: OCMModule, state: Boolean) { + if (state) { + if (eventRegistry.registerListener(module)) { + Messenger.debug("Enabled " + module.moduleName) + } + } else { + if (eventRegistry.unregisterListener(module)) { + Messenger.debug("Disabled " + module.moduleName) + } + } + } + +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/OCMConfigHandler.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/OCMConfigHandler.kt new file mode 100644 index 00000000..7774653e --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/OCMConfigHandler.kt @@ -0,0 +1,83 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics + +import kernitus.plugin.OldCombatMechanics.utilities.Config +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector +import org.bukkit.configuration.file.YamlConfiguration +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.util.* + +class OCMConfigHandler(private val plugin: OCMMain) { + private val CONFIG_NAME = "config.yml" + + fun upgradeConfig() { + // Remove old backup file if present + val backup = getFile("config-backup.yml") + if (backup.exists()) backup.delete() + + // Keeping YAML comments not available in lower versions + if (Reflector.versionIsNewerOrEqualTo(1, 18, 1) || + Config.getConfig().getBoolean("force-below-1-18-1-config-upgrade", false) + ) { + plugin.logger.warning("Config version does not match, upgrading old config") + // Load values from old config + val oldConfig = getConfig(CONFIG_NAME) + val defaultConfig = YamlConfiguration.loadConfiguration( + InputStreamReader(Objects.requireNonNull(plugin.getResource(CONFIG_NAME))) + ) + + // Copies value from old config if present in new config + for (key in defaultConfig.getKeys(true)) { + if (key == "config-version" || !oldConfig.contains(key)) continue + + if (defaultConfig.isConfigurationSection(key)) continue // Only get leaf keys + + + defaultConfig[key] = oldConfig[key] + } + try { + // Overwrites old file if needed + defaultConfig.save(getFile(CONFIG_NAME)) + plugin.logger.info("Config has been updated") + } catch (e: IOException) { + plugin.logger.severe("Failed to upgrade config") + } + } else { + plugin.logger.warning("Config version does not match, backing up old config and creating a new one") + // Change name of old config + val configFile = getFile(CONFIG_NAME) + configFile.renameTo(backup) + } + + // Save new version if none is present + setupConfigIfNotPresent() + } + + /** + * Generates new config.yml file, if not present. + */ + fun setupConfigIfNotPresent() { + if (!doesConfigExist()) { + plugin.saveDefaultConfig() + plugin.logger.info("Config file generated") + } + } + + fun getConfig(fileName: String): YamlConfiguration { + return YamlConfiguration.loadConfiguration(getFile(fileName)) + } + + fun getFile(fileName: String): File { + return File(plugin.dataFolder, fileName.replace('/', File.separatorChar)) + } + + fun doesConfigExist(): Boolean { + return getFile(CONFIG_NAME).exists() + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/OCMMain.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/OCMMain.kt new file mode 100644 index 00000000..e21395fc --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/OCMMain.kt @@ -0,0 +1,267 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics + +import com.comphenix.protocol.ProtocolLibrary +import com.comphenix.protocol.ProtocolManager +import kernitus.plugin.OldCombatMechanics.ModuleLoader.addModule +import kernitus.plugin.OldCombatMechanics.ModuleLoader.initialise +import kernitus.plugin.OldCombatMechanics.commands.OCMCommandCompleter +import kernitus.plugin.OldCombatMechanics.commands.OCMCommandHandler +import kernitus.plugin.OldCombatMechanics.hooks.PlaceholderAPIHook +import kernitus.plugin.OldCombatMechanics.hooks.api.Hook +import kernitus.plugin.OldCombatMechanics.module.* +import kernitus.plugin.OldCombatMechanics.updater.ModuleUpdateChecker +import kernitus.plugin.OldCombatMechanics.utilities.Config +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import kernitus.plugin.OldCombatMechanics.utilities.damage.AttackCooldownTracker +import kernitus.plugin.OldCombatMechanics.utilities.damage.EntityDamageByEntityListener +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector +import kernitus.plugin.OldCombatMechanics.utilities.storage.ModesetListener +import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage +import org.bstats.bukkit.Metrics +import org.bstats.charts.SimpleBarChart +import org.bstats.charts.SimplePie +import org.bukkit.Bukkit +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player +import org.bukkit.event.EventException +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.plugin.RegisteredListener +import org.bukkit.plugin.java.JavaPlugin +import java.util.* +import java.util.function.Consumer +import java.util.stream.Collectors + +class OCMMain : JavaPlugin() { + private val logger = getLogger() + private val CH = OCMConfigHandler(this) + private val disableListeners: MutableList = ArrayList() + private val enableListeners: MutableList = ArrayList() + private val hooks: MutableList = ArrayList() + var protocolManager: ProtocolManager? = null + private set + + override fun onEnable() { + instance = this + + // Setting up config.yml + CH.setupConfigIfNotPresent() + + // Initialise persistent player storage + PlayerStorage.initialise(this) + + // Initialise ModuleLoader utility + initialise(this) + + // Initialise Config utility + Config.initialise(this) + + // Initialise the Messenger utility + Messenger.initialise(this) + + try { + if (server.pluginManager.getPlugin("ProtocolLib") != null && + server.pluginManager.getPlugin("ProtocolLib")!!.isEnabled + ) protocolManager = ProtocolLibrary.getProtocolManager() + } catch (e: Exception) { + Messenger.warn("No ProtocolLib detected, some features might be disabled") + } + + // Register all the modules + registerModules() + + // Register all hooks for integrating with other plugins + registerHooks() + + // Initialise all the hooks + hooks.forEach(Consumer { hook: Hook -> hook.init(this) }) + + // Set up the command handler + getCommand("OldCombatMechanics")!!.setExecutor(OCMCommandHandler(this)) + // Set up command tab completer + getCommand("OldCombatMechanics")!!.tabCompleter = OCMCommandCompleter() + + Config.reload() + + // BStats Metrics + val metrics = Metrics(this, 53) + + // Simple bar chart + metrics.addCustomChart( + SimpleBarChart( + "enabled_modules" + ) { + ModuleLoader.modules.stream() + .filter { obj: OCMModule -> obj.isEnabled() } + .collect( + Collectors.toMap( + { obj: OCMModule -> obj.toString() }, + { 1 }) + ) + } + ) + + // Pie chart of enabled/disabled for each module + ModuleLoader.modules.forEach(Consumer { module: OCMModule -> + metrics.addCustomChart( + SimplePie( + module.moduleName + "_pie" + ) { if (module.isEnabled()) "enabled" else "disabled" }) + }) + + enableListeners.forEach(Consumer { obj: Runnable -> obj.run() }) + + // Properly handle Plugman load/unload. + val joinListeners = Arrays.stream(PlayerJoinEvent.getHandlerList().registeredListeners) + .filter { registeredListener: RegisteredListener -> registeredListener.plugin == this } + .collect(Collectors.toList()) + + Bukkit.getOnlinePlayers().forEach { player: Player? -> + val event = PlayerJoinEvent(player!!, "") + // Trick all the modules into thinking the player just joined in case the plugin was loaded with Plugman. + // This way attack speeds, item modifications, etc. will be applied immediately instead of after a re-log. + joinListeners.forEach(Consumer { registeredListener: RegisteredListener -> + try { + registeredListener.callEvent(event) + } catch (e: EventException) { + e.printStackTrace() + } + }) + } + + // Logging to console the enabling of OCM + val pdfFile = this.description + logger.info(pdfFile.name + " v" + pdfFile.version + " has been enabled") + + if (Config.moduleEnabled("update-checker")) Bukkit.getScheduler().runTaskLaterAsynchronously( + this, + Runnable { UpdateChecker(this).performUpdate() }, 20L + ) + + metrics.addCustomChart( + SimplePie( + "auto_update_pie" + ) { + if (Config.moduleSettingEnabled( + "update-checker", + "auto-update" + ) + ) "enabled" else "disabled" + }) + } + + override fun onDisable() { + val pdfFile = this.description + + disableListeners.forEach(Consumer { obj: Runnable -> obj.run() }) + + // Properly handle Plugman load/unload. + val quitListeners = Arrays.stream(PlayerQuitEvent.getHandlerList().registeredListeners) + .filter { registeredListener: RegisteredListener -> registeredListener.plugin == this } + .collect(Collectors.toList()) + + // Trick all the modules into thinking the player just quit in case the plugin was unloaded with Plugman. + // This way attack speeds, item modifications, etc. will be restored immediately instead of after a disconnect. + Bukkit.getOnlinePlayers().forEach { player: Player? -> + val event = PlayerQuitEvent(player!!, "") + quitListeners.forEach(Consumer { registeredListener: RegisteredListener -> + try { + registeredListener.callEvent(event) + } catch (e: EventException) { + e.printStackTrace() + } + }) + } + + PlayerStorage.instantSave() + + // Logging to console the disabling of OCM + logger.info(pdfFile.name + " v" + pdfFile.version + " has been disabled") + } + + private fun registerModules() { + // Update Checker (also a module, so we can use the dynamic registering/unregistering) + addModule(ModuleUpdateChecker(this)) + + // Modeset listener, for when player joins or changes world + addModule(ModesetListener(this)) + + // Module listeners + addModule(ModuleAttackCooldown(this)) + + // If below 1.16, we need to keep track of player attack cooldown ourselves + if (Reflector.getMethod(HumanEntity::class.java, "getAttackCooldown", 0) == null) { + addModule(AttackCooldownTracker(this)) + } + + //Listeners registered later with same priority are called later + + //These four listen to OCMEntityDamageByEntityEvent: + addModule(ModuleOldToolDamage(this)) + addModule(ModuleSwordSweep(this)) + addModule(ModuleOldPotionEffects(this)) + addModule(ModuleOldCriticalHits(this)) + + //Next block are all on LOWEST priority, so will be called in the following order: + // Damage order: base -> potion effects -> critical hit -> enchantments + // Defence order: overdamage -> blocking -> armour -> resistance -> armour enchs -> absorption + //EntityDamageByEntityListener calls OCMEntityDamageByEntityEvent, see modules above + // For everything from base to overdamage + addModule(EntityDamageByEntityListener(this)) + // ModuleSwordBlocking to calculate blocking + addModule(ModuleShieldDamageReduction(this)) + // OldArmourStrength for armour -> resistance -> armour enchs -> absorption + addModule(ModuleOldArmourStrength(this)) + + addModule(ModuleSwordBlocking(this)) + addModule(ModuleOldArmourDurability(this)) + + addModule(ModuleGoldenApple(this)) + addModule(ModuleFishingKnockback(this)) + addModule(ModulePlayerKnockback(this)) + addModule(ModulePlayerRegen(this)) + + addModule(ModuleDisableCrafting(this)) + addModule(ModuleDisableOffHand(this)) + addModule(ModuleOldBrewingStand(this)) + addModule(ModuleProjectileKnockback(this)) + addModule(ModuleNoLapisEnchantments(this)) + addModule(ModuleDisableEnderpearlCooldown(this)) + addModule(ModuleChorusFruit(this)) + + addModule(ModuleOldBurnDelay(this)) + addModule(ModuleAttackFrequency(this)) + addModule(ModuleFishingRodVelocity(this)) + + // These modules require ProtocolLib + if (protocolManager != null) { + addModule(ModuleAttackSounds(this)) + addModule(ModuleSwordSweepParticles(this)) + } else { + Messenger.warn("No ProtocolLib detected, attack-sounds and sword-sweep-particles modules will be disabled") + } + } + + private fun registerHooks() { + if (server.pluginManager.isPluginEnabled("PlaceholderAPI")) hooks.add(PlaceholderAPIHook()) + } + + fun upgradeConfig() = CH.upgradeConfig() + + fun doesConfigExist() = CH.doesConfigExist() + + public override fun getFile() = super.getFile() + + companion object { + var instance: OCMMain? = null + private set + + val version: String + get() = instance!!.description.version + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/UpdateChecker.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/UpdateChecker.kt new file mode 100644 index 00000000..2283cb82 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/UpdateChecker.kt @@ -0,0 +1,54 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics + +import kernitus.plugin.OldCombatMechanics.updater.SpigetUpdateChecker +import kernitus.plugin.OldCombatMechanics.utilities.Config +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector +import org.bukkit.ChatColor +import org.bukkit.entity.Player +import java.util.function.Consumer + +class UpdateChecker(private val plugin: OCMMain) { + private val updater = SpigetUpdateChecker() + + // We don't really want to auto update if the config is not going to be upgraded automatically + private val autoDownload = + Config.moduleSettingEnabled("update-checker", "auto-update") && + (Reflector.versionIsNewerOrEqualTo(1, 18, 1) || + Config.getConfig().getBoolean("force-below-1-18-1-config-upgrade", false) + ) + + + fun performUpdate(player: Player? = null) { + if (player != null) update { message: String? -> player.sendMessage(message) } + else update { message: String -> Messenger.info(message) } + } + + private fun update(target: Consumer) { + val messages: MutableList = ArrayList() + if (updater.isUpdateAvailable) { + messages.add(ChatColor.BLUE.toString() + "An update for OldCombatMechanics to version " + updater.latestVersion + " is available!") + if (!autoDownload) { + messages.add(ChatColor.BLUE.toString() + "Click here to download it: " + ChatColor.GRAY + updater.updateURL) + } else { + messages.add(ChatColor.BLUE.toString() + "Downloading update: " + ChatColor.GRAY + updater.updateURL) + try { + if (updater.downloadLatestVersion(plugin.server.updateFolderFile, plugin.file.name)) messages.add( + ChatColor.GREEN.toString() + "Update downloaded. Restart or reload server to enable new version." + ) + else throw RuntimeException() + } catch (e: Exception) { + messages.add(ChatColor.RED.toString() + "Error occurred while downloading update! Check console for more details") + e.printStackTrace() + } + } + } + + messages.forEach(target) + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandCompleter.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandCompleter.kt index 4f126eaa..86ec8bd8 100644 --- a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandCompleter.kt +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandCompleter.kt @@ -42,7 +42,7 @@ class OCMCommandCompleter : TabCompleter { if (sender is Player) { // Get the modesets allowed in the world player is in val world = sender.world completions.addAll( - Config.getWorlds() // If world not in config, all modesets allowed + Config.worlds // If world not in config, all modesets allowed .getOrDefault(world.uid, Config.getModesets().keys) .stream() .filter { ms: String -> ms.startsWith(args[1]) } diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandHandler.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandHandler.kt index b3b53543..7f2c1a2c 100644 --- a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandHandler.kt +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/commands/OCMCommandHandler.kt @@ -41,9 +41,8 @@ class OCMCommandHandler(private val plugin: OCMMain) : CommandExecutor { if (checkPermissions(sender, Subcommand.mode)) Messenger.sendNoPrefix( sender, Config.getConfig().getString( - "mode-messages.message-usage", - "&4ERROR: &rmode-messages.message-usage string missing" - ) + "mode-messages.message-usage" + ) ?: "&4ERROR: &rmode-messages.message-usage string missing" ) Messenger.sendNoPrefix(sender, ChatColor.DARK_GRAY.toString() + Messenger.HORIZONTAL_BAR) @@ -63,19 +62,15 @@ class OCMCommandHandler(private val plugin: OCMMain) : CommandExecutor { Messenger.send( sender, - Config.getConfig().getString( - "mode-messages.mode-status", - "&4ERROR: &rmode-messages.mode-status string missing" - ), + Config.getConfig().getString("mode-messages.mode-status") + ?: "&4ERROR: &rmode-messages.mode-status string missing", modeName ) } Messenger.send( sender, - Config.getConfig().getString( - "mode-messages.message-usage", - "&4ERROR: &rmode-messages.message-usage string missing" - ) + Config.getConfig().getString("mode-messages.message-usage") + ?: "&4ERROR: &rmode-messages.message-usage string missing" ) return } @@ -85,10 +80,8 @@ class OCMCommandHandler(private val plugin: OCMMain) : CommandExecutor { if (!Config.getModesets().containsKey(modesetName)) { Messenger.send( sender, - Config.getConfig().getString( - "mode-messages.invalid-modeset", - "&4ERROR: &rmode-messages.invalid-modeset string missing" - ) + Config.getConfig().getString("mode-messages.invalid-modeset") + ?: "&4ERROR: &rmode-messages.invalid-modeset string missing" ) return } @@ -100,10 +93,8 @@ class OCMCommandHandler(private val plugin: OCMMain) : CommandExecutor { } else { Messenger.send( sender, - Config.getConfig().getString( - "mode-messages.invalid-player", - "&4ERROR: &rmode-messages.invalid-player string missing" - ) + Config.getConfig().getString("mode-messages.invalid-player") + ?: "&4ERROR: &rmode-messages.invalid-player string missing" ) return } @@ -112,25 +103,21 @@ class OCMCommandHandler(private val plugin: OCMMain) : CommandExecutor { if (player == null) { Messenger.send( sender, - Config.getConfig().getString( - "mode-messages.invalid-player", - "&4ERROR: &rmode-messages.invalid-player string missing" - ) + Config.getConfig().getString("mode-messages.invalid-player") + ?: "&4ERROR: &rmode-messages.invalid-player string missing" ) return } val worldId = player.world.uid - val worldModesets = Config.getWorlds()[worldId] + val worldModesets = Config.worlds[worldId] // If modesets null it means not configured, so all are allowed if (worldModesets != null && !worldModesets.contains(modesetName)) { // Modeset not allowed in current world Messenger.send( sender, - Config.getConfig().getString( - "mode-messages.invalid-modeset", - "&4ERROR: &rmode-messages.invalid-modeset string missing" - ) + Config.getConfig().getString("mode-messages.invalid-modeset") + ?: "&4ERROR: &rmode-messages.invalid-modeset string missing" ) return } @@ -142,16 +129,14 @@ class OCMCommandHandler(private val plugin: OCMMain) : CommandExecutor { Messenger.send( sender, - Config.getConfig().getString( - "mode-messages.mode-set", - "&4ERROR: &rmode-messages.mode-set string missing" - ), + Config.getConfig().getString("mode-messages.mode-set") + ?: "&4ERROR: &rmode-messages.mode-set string missing", modesetName ) // Re-apply things like attack speed and collision team val playerCopy: Player = player - ModuleLoader.getModules().forEach(Consumer { module: OCMModule -> module.onModesetChange(playerCopy) }) + ModuleLoader.modules.forEach(Consumer { module: OCMModule -> module.onModesetChange(playerCopy) }) } /* diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/hooks/PlaceholderAPIHook.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/hooks/PlaceholderAPIHook.kt new file mode 100644 index 00000000..38aa5c3f --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/hooks/PlaceholderAPIHook.kt @@ -0,0 +1,84 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.hooks + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.hooks.api.Hook +import kernitus.plugin.OldCombatMechanics.module.ModuleDisableEnderpearlCooldown +import kernitus.plugin.OldCombatMechanics.module.ModuleGoldenApple +import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage +import me.clip.placeholderapi.expansion.PlaceholderExpansion +import org.bukkit.entity.Player + +class PlaceholderAPIHook : Hook { + private var expansion: PlaceholderExpansion? = null + + override fun init(plugin: OCMMain) { + expansion = object : PlaceholderExpansion() { + override fun canRegister(): Boolean { + return true + } + + override fun persist(): Boolean { + return true + } + + override fun getIdentifier(): String { + return "ocm" + } + + override fun getAuthor(): String { + return java.lang.String.join(", ", plugin.description.authors) + } + + override fun getVersion(): String { + return plugin.description.version + } + + override fun onPlaceholderRequest(player: Player, identifier: String): String? { + + when (identifier) { + "modeset" -> return getModeset(player) + "gapple_cooldown" -> return getGappleCooldown(player) + "napple_cooldown" -> return getNappleCooldown(player) + "enderpearl_cooldown" -> return getEnderpearlCooldown(player) + } + + return null + } + + fun getGappleCooldown(player: Player): String { + val seconds = ModuleGoldenApple.instance.getGappleCooldown(player.uniqueId) + return if (seconds > 0) seconds.toString() else "None" + } + + fun getNappleCooldown(player: Player): String { + val seconds = ModuleGoldenApple.instance.getNappleCooldown(player.uniqueId) + return if (seconds > 0) seconds.toString() else "None" + } + + fun getEnderpearlCooldown(player: Player): String { + val seconds = ModuleDisableEnderpearlCooldown.instance.getEnderpearlCooldown(player.uniqueId) + return if (seconds > 0) seconds.toString() else "None" + } + + fun getModeset(player: Player): String { + val playerData = PlayerStorage.getPlayerData(player.uniqueId) + var modeName = playerData.getModesetForWorld(player.world.uid) + if (modeName == null || modeName.isEmpty()) modeName = "unknown" + return modeName + } + } + + (expansion as PlaceholderExpansion).register() + } + + override fun deinit(plugin: OCMMain) { + if (expansion != null) { + expansion!!.unregister() + } + } +} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/hooks/api/Hook.java b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/hooks/api/Hook.kt similarity index 52% rename from src/main/java/kernitus/plugin/OldCombatMechanics/hooks/api/Hook.java rename to src/main/kotlin/kernitus/plugin/OldCombatMechanics/hooks/api/Hook.kt index f3d5356b..9d7817f5 100644 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/hooks/api/Hook.java +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/hooks/api/Hook.kt @@ -3,12 +3,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package kernitus.plugin.OldCombatMechanics.hooks.api; +package kernitus.plugin.OldCombatMechanics.hooks.api -import kernitus.plugin.OldCombatMechanics.OCMMain; +import kernitus.plugin.OldCombatMechanics.OCMMain -public interface Hook { - void init(OCMMain plugin); +interface Hook { + fun init(plugin: OCMMain) - void deinit(OCMMain plugin); + fun deinit(plugin: OCMMain) } diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackCooldown.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackCooldown.kt new file mode 100644 index 00000000..0cca2a7f --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackCooldown.kt @@ -0,0 +1,91 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.getPlayerData +import org.bukkit.Bukkit +import org.bukkit.attribute.Attribute +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.player.PlayerChangedWorldEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent + +/** + * Disables the attack cooldown. + */ +class ModuleAttackCooldown(plugin: OCMMain) : OCMModule(plugin, "disable-attack-cooldown") { + private val NEW_ATTACK_SPEED = 4.0 + + override fun reload() { + Bukkit.getOnlinePlayers().forEach { player: Player -> this.adjustAttackSpeed(player) } + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPlayerLogin(e: PlayerJoinEvent) { + adjustAttackSpeed(e.player) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onWorldChange(e: PlayerChangedWorldEvent) { + adjustAttackSpeed(e.player) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPlayerQuit(e: PlayerQuitEvent) { + setAttackSpeed(e.player, NEW_ATTACK_SPEED) + } + + /** + * Adjusts the attack speed to the default or configured value, depending on whether the module is enabled. + * + * @param player the player to set the attack speed for + */ + private fun adjustAttackSpeed(player: Player) { + val attackSpeed = if (isEnabled(player)) + module()!!.getDouble("generic-attack-speed") + else + NEW_ATTACK_SPEED + + setAttackSpeed(player, attackSpeed) + } + + override fun onModesetChange(player: Player) { + adjustAttackSpeed(player) + } + + /** + * Sets the attack speed to the given value. + * + * @param player the player to set it for + * @param attackSpeed the attack speed to set it to + */ + fun setAttackSpeed(player: Player, attackSpeed: Double) { + val attribute = player.getAttribute(Attribute.GENERIC_ATTACK_SPEED) ?: return + + val baseValue = attribute.baseValue + + val modesetName = getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid) + debug( + String.format( + "Setting attack speed to %.2f (was: %.2f) for %s in mode %s", + attackSpeed, + baseValue, + player.name, + modesetName + ) + ) + + if (baseValue != attackSpeed) { + debug(String.format("Setting attack speed to %.2f (was: %.2f)", attackSpeed, baseValue), player) + + attribute.baseValue = attackSpeed + player.saveData() + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackFrequency.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackFrequency.kt new file mode 100644 index 00000000..c0096a54 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackFrequency.kt @@ -0,0 +1,97 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import org.bukkit.Bukkit +import org.bukkit.World +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.entity.CreatureSpawnEvent +import org.bukkit.event.entity.EntityTeleportEvent +import org.bukkit.event.player.PlayerChangedWorldEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.player.PlayerRespawnEvent +import java.util.function.Consumer + +class ModuleAttackFrequency(plugin: OCMMain) : OCMModule(plugin, "attack-frequency") { + init { + reload() + } + + override fun reload() { + playerDelay = module()!!.getInt("playerDelay") + mobDelay = module()!!.getInt("mobDelay") + + Bukkit.getWorlds().forEach(Consumer { world: World -> + world.livingEntities.forEach( + Consumer { livingEntity: LivingEntity -> + if (livingEntity is Player) livingEntity.setMaximumNoDamageTicks( + if (isEnabled( + livingEntity + ) + ) playerDelay else DEFAULT_DELAY + ) + else livingEntity.maximumNoDamageTicks = if (isEnabled(world)) mobDelay else DEFAULT_DELAY + }) + }) + } + + @EventHandler + fun onPlayerJoin(e: PlayerJoinEvent) { + val player = e.player + if (isEnabled(player)) setDelay(player, playerDelay) + } + + @EventHandler + fun onPlayerLogout(e: PlayerQuitEvent) { + setDelay(e.player, DEFAULT_DELAY) + } + + @EventHandler + fun onPlayerChangeWorld(e: PlayerChangedWorldEvent) { + val player = e.player + setDelay(player, if (isEnabled(player)) playerDelay else DEFAULT_DELAY) + } + + @EventHandler + fun onPlayerRespawn(e: PlayerRespawnEvent) { + val player = e.player + setDelay(player, if (isEnabled(player)) playerDelay else DEFAULT_DELAY) + } + + private fun setDelay(player: Player, delay: Int) { + player.maximumNoDamageTicks = delay + debug("Set hit delay to $delay", player) + } + + @EventHandler + fun onCreatureSpawn(e: CreatureSpawnEvent) { + val livingEntity = e.entity + val world = livingEntity.world + if (isEnabled(world)) livingEntity.maximumNoDamageTicks = mobDelay + } + + @EventHandler + fun onEntityTeleportEvent(e: EntityTeleportEvent) { + // This event is only fired for non-player entities + val entity = e.entity as? LivingEntity ?: return + + val fromWorld = e.from.world + val toLocation = e.to ?: return + val toWorld = toLocation.world + if (fromWorld!!.uid !== toWorld!!.uid) entity.maximumNoDamageTicks = + if (isEnabled(toWorld)) mobDelay else DEFAULT_DELAY + } + + companion object { + private const val DEFAULT_DELAY = 20 + private var playerDelay = 0 + private var mobDelay = 0 + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackSounds.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackSounds.kt new file mode 100644 index 00000000..bf3d8ddf --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleAttackSounds.kt @@ -0,0 +1,79 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import com.comphenix.protocol.PacketType +import com.comphenix.protocol.events.PacketAdapter +import com.comphenix.protocol.events.PacketEvent +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.warn +import org.bukkit.plugin.Plugin + +/** + * A module to disable the new attack sounds. + */ +class ModuleAttackSounds(plugin: OCMMain) : OCMModule(plugin, "disable-attack-sounds") { + private val protocolManager = plugin.protocolManager + private val soundListener = SoundListener(plugin) + private val blockedSounds: MutableSet = HashSet(getBlockedSounds()) + + init { + reload() + } + + override fun reload() { + blockedSounds.clear() + blockedSounds.addAll(getBlockedSounds()) + + if (isEnabled()) protocolManager!!.addPacketListener(soundListener) + else protocolManager!!.removePacketListener(soundListener) + } + + private fun getBlockedSounds(): Collection { + return module()!!.getStringList("blocked-sound-names") + } + + /** + * Disables attack sounds. + */ + private inner class SoundListener(plugin: Plugin?) : + PacketAdapter(plugin, PacketType.Play.Server.NAMED_SOUND_EFFECT) { + private var disabledDueToError = false + + override fun onPacketSending(packetEvent: PacketEvent) { + if (disabledDueToError || !isEnabled(packetEvent.player)) return + + try { + val packetContainer = packetEvent.packet + val sound = packetContainer.soundEffects.read(0) ?: return + + + //fix NullpointerException when sending a custom sound + + val soundName = sound.toString() // Works for both string and namespaced key + + if (blockedSounds.contains(soundName)) { + packetEvent.isCancelled = true + debug("Blocked sound $soundName", packetEvent.player) + } + } catch (e: Exception) { + disabledDueToError = true + warn( + e, + "Error detecting sound packets. Please report it along with the following exception " + + "on github." + ) + } catch (e: ExceptionInInitializerError) { + disabledDueToError = true + warn( + e, + "Error detecting sound packets. Please report it along with the following exception " + + "on github." + ) + } + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleChorusFruit.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleChorusFruit.kt new file mode 100644 index 00000000..a67ae00a --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleChorusFruit.kt @@ -0,0 +1,94 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.MathsHelper.clamp +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.event.EventHandler +import org.bukkit.event.player.PlayerItemConsumeEvent +import org.bukkit.event.player.PlayerTeleportEvent +import java.util.concurrent.ThreadLocalRandom +import kotlin.math.min + +/** + * A module to control chorus fruits. + */ +class ModuleChorusFruit(plugin: OCMMain) : OCMModule(plugin, "chorus-fruit") { + @EventHandler + fun onEat(e: PlayerItemConsumeEvent) { + if (e.item.type != Material.CHORUS_FRUIT) return + val player = e.player + + if (!isEnabled(player)) return + + if (module()!!.getBoolean("prevent-eating")) { + e.isCancelled = true + return + } + + val hungerValue = module()!!.getInt("hunger-value") + val saturationValue = module()!!.getDouble("saturation-value") + val previousFoodLevel = player.foodLevel + val previousSaturation = player.saturation + + // Run it on the next tick to reset things while not cancelling the chorus fruit eat event + // This ensures the teleport event is fired and counts towards statistics + Bukkit.getScheduler().runTaskLater(plugin, Runnable { + val newFoodLevel = min((hungerValue + previousFoodLevel).toDouble(), 20.0).toInt() + val newSaturation = + min((saturationValue + previousSaturation).toFloat().toDouble(), newFoodLevel.toDouble()).toFloat() + + player.foodLevel = newFoodLevel + player.saturation = newSaturation + debug("Food level changed from: " + previousFoodLevel + " to " + player.foodLevel, player) + }, 2L) + } + + @EventHandler + fun onTeleport(e: PlayerTeleportEvent) { + if (e.cause != PlayerTeleportEvent.TeleportCause.CHORUS_FRUIT) return + + val player = e.player + if (!isEnabled(player)) return + + val distance = maxTeleportationDistance + + if (distance == 8.0) { + debug("Using vanilla teleport implementation!", player) + return + } + + if (distance <= 0) { + debug("Chorus teleportation is not allowed", player) + e.isCancelled = true + return + } + + // Not sure when this can occur, but it is marked as @Nullable + val toLocation = e.to + + if (toLocation == null) { + debug("Teleport target is null", player) + return + } + + val maxheight = toLocation.world!!.maxHeight + + e.setTo( + player.location.add( + ThreadLocalRandom.current().nextDouble(-distance, distance), + clamp(ThreadLocalRandom.current().nextDouble(-distance, distance), 0.0, (maxheight - 1).toDouble()), + ThreadLocalRandom.current().nextDouble(-distance, distance) + ) + ) + } + + + private val maxTeleportationDistance: Double + get() = module()!!.getDouble("max-teleportation-distance") +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableCrafting.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableCrafting.kt new file mode 100644 index 00000000..bc96512f --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableCrafting.kt @@ -0,0 +1,54 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils.loadMaterialList +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.send +import org.bukkit.Material +import org.bukkit.entity.HumanEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.inventory.PrepareItemCraftEvent +import java.util.function.Consumer + +/** + * Makes the specified materials uncraftable. + */ +class ModuleDisableCrafting(plugin: OCMMain) : OCMModule(plugin, "disable-crafting") { + private var denied: List? = null + private var message: String? = null + + init { + reload() + } + + override fun reload() { + denied = loadMaterialList(module()!!, "denied") + message = if (module()!!.getBoolean("showMessage")) module()!!.getString("message") else null + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPrepareItemCraft(e: PrepareItemCraftEvent) { + val viewers = e.viewers + if (viewers.size == 0) return + + if (!isEnabled(viewers[0])) return + + val inv = e.inventory + val result = inv.result + + if (result != null && denied!!.contains(result.type)) { + inv.result = null + if (message != null) viewers.forEach(Consumer { viewer: HumanEntity? -> + send( + viewer!!, + message!! + ) + }) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableEnderpearlCooldown.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableEnderpearlCooldown.kt new file mode 100644 index 00000000..f3cfe3e7 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableEnderpearlCooldown.kt @@ -0,0 +1,134 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.send +import org.bukkit.GameMode +import org.bukkit.Material +import org.bukkit.entity.EnderPearl +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.ProjectileLaunchEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.inventory.ItemStack +import java.util.* +import kotlin.math.max + +/** + * Allows you to throw enderpearls as often as you like, not only after a cooldown. + */ +class ModuleDisableEnderpearlCooldown(plugin: OCMMain) : OCMModule(plugin, "disable-enderpearl-cooldown") { + /** + * Contains players that threw an ender pearl. As the handler calls launchProjectile, + * which also calls ProjectileLaunchEvent, we need to ignore that event call. + */ + private val ignoredPlayers: MutableSet = HashSet() + private var lastLaunched: MutableMap? = null + private var cooldown = 0 + private var message: String? = null + + init { + instance = this + reload() + } + + override fun reload() { + cooldown = module().getInt("cooldown") + if (cooldown > 0) { + if (lastLaunched == null) lastLaunched = WeakHashMap() + } else lastLaunched = null + + message = if (module().getBoolean("showMessage")) module().getString("message") else null + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPlayerShoot(e: ProjectileLaunchEvent) { + if (e.isCancelled) return // For compatibility with other plugins + + + val projectile = e.entity as? EnderPearl ?: return + val shooter = projectile.shooter as? Player ?: return + + if (!isEnabled(shooter)) return + + val uuid = shooter.uniqueId + + if (ignoredPlayers.contains(uuid)) return + + e.isCancelled = true + + // Check if the cooldown has expired yet + if (lastLaunched != null) { + val currentTime = System.currentTimeMillis() / 1000 + if (lastLaunched!!.containsKey(uuid)) { + val elapsedSeconds = currentTime - lastLaunched!![uuid]!! + if (elapsedSeconds < cooldown) { + if (message != null) send( + shooter, + message!!, cooldown - elapsedSeconds + ) + return + } + } + + lastLaunched!![uuid] = currentTime + } + + // Make sure we ignore the event triggered by launchProjectile + ignoredPlayers.add(uuid) + val pearl = shooter.launchProjectile(EnderPearl::class.java) + ignoredPlayers.remove(uuid) + + pearl.velocity = shooter.eyeLocation.direction.multiply(2) + + if (shooter.gameMode == GameMode.CREATIVE) return + + val enderpearlItemStack: ItemStack + val playerInventory = shooter.inventory + val mainHand = playerInventory.itemInMainHand + val offHand = playerInventory.itemInOffHand + + enderpearlItemStack = if (isEnderPearl(mainHand)) mainHand + else if (isEnderPearl(offHand)) offHand + else return + + enderpearlItemStack.amount -= 1 + } + + private fun isEnderPearl(itemStack: ItemStack?): Boolean { + return itemStack != null && itemStack.type == Material.ENDER_PEARL + } + + @EventHandler + fun onPlayerQuit(e: PlayerQuitEvent) { + if (lastLaunched != null) lastLaunched!!.remove(e.player.uniqueId) + } + + /** + * Get the remaining cooldown time for ender pearls for a given player. + * @param playerUUID The UUID of the player to check the cooldown for. + * @return The remaining cooldown time in seconds, or 0 if there is no cooldown or it has expired. + */ + fun getEnderpearlCooldown(playerUUID: UUID): Long { + if (lastLaunched != null && lastLaunched!!.containsKey(playerUUID)) { + val currentTime = System.currentTimeMillis() / 1000 // Current time in seconds + val lastLaunchTime = lastLaunched!![playerUUID]!! // Last launch time in seconds + val elapsedSeconds = currentTime - lastLaunchTime + val cooldownRemaining = cooldown - elapsedSeconds + return max( + cooldownRemaining.toDouble(), + 0.0 + ).toLong() // Return the remaining cooldown or 0 if it has expired + } + return 0 + } + + companion object { + lateinit var instance: ModuleDisableEnderpearlCooldown + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableOffHand.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableOffHand.kt new file mode 100644 index 00000000..a381e0f5 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleDisableOffHand.kt @@ -0,0 +1,166 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils.loadMaterialList +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.send +import org.bukkit.Material +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import org.bukkit.event.Event +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.inventory.ClickType +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryDragEvent +import org.bukkit.event.inventory.InventoryType +import org.bukkit.event.player.PlayerChangedWorldEvent +import org.bukkit.event.player.PlayerSwapHandItemsEvent +import org.bukkit.inventory.ItemStack +import java.util.function.BiPredicate + +/** + * Disables usage of the off-hand. + */ +class ModuleDisableOffHand(plugin: OCMMain) : OCMModule(plugin, "disable-offhand") { + private var materials: List? = null + private var deniedMessage: String? = null + private var blockType: BlockType? = null + + init { + reload() + } + + override fun reload() { + blockType = if (module()!!.getBoolean("whitelist")) BlockType.WHITELIST else BlockType.BLACKLIST + materials = loadMaterialList(module()!!, "items") + deniedMessage = module()!!.getString("denied-message") + } + + private fun sendDeniedMessage(sender: CommandSender) { + if (!deniedMessage!!.trim { it <= ' ' }.isEmpty()) send( + sender, + deniedMessage!! + ) + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun onSwapHandItems(e: PlayerSwapHandItemsEvent) { + val player = e.player + if (isEnabled(player) && isItemBlocked(e.offHandItem)) { + e.isCancelled = true + sendDeniedMessage(player) + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun onInventoryClick(e: InventoryClickEvent) { + val player = e.whoClicked + if (!isEnabled(player)) return + val clickType = e.click + + try { + if (clickType == ClickType.SWAP_OFFHAND) { + e.result = Event.Result.DENY + sendDeniedMessage(player) + return + } + } catch (ignored: NoSuchFieldError) { + } // For versions below 1.16 + + + val clickedInventory = e.clickedInventory ?: return + val inventoryType = clickedInventory.type + // Source inventory must be PLAYER + if (inventoryType != InventoryType.PLAYER) return + + val view = e.view + // If neither of the inventories is CRAFTING, player cannot be moving stuff to the offhand + if (view.bottomInventory.type != InventoryType.CRAFTING && + view.topInventory.type != InventoryType.CRAFTING + ) return + + // Prevent shift-clicking a shield into the offhand item slot + val currentItem = e.currentItem + if (currentItem != null && currentItem.type == Material.SHIELD && isItemBlocked(currentItem) + && e.slot != OFFHAND_SLOT && e.isShiftClick + ) { + e.result = Event.Result.DENY + sendDeniedMessage(player) + } + + if (e.slot == OFFHAND_SLOT && // Let allowed items be placed into offhand slot with number keys (hotbar swap) + ((clickType == ClickType.NUMBER_KEY && isItemBlocked(clickedInventory.getItem(e.hotbarButton))) + || isItemBlocked(e.cursor)) // Deny placing not allowed items into offhand slot + ) { + e.result = Event.Result.DENY + sendDeniedMessage(player) + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun onInventoryDrag(e: InventoryDragEvent) { + val player = e.whoClicked + if (!isEnabled(player) || e.inventory.type != InventoryType.CRAFTING || !e.inventorySlots.contains(OFFHAND_SLOT)) return + + if (isItemBlocked(e.oldCursor)) { + e.result = Event.Result.DENY + sendDeniedMessage(player) + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun onWorldChange(e: PlayerChangedWorldEvent) { + onModesetChange(e.player) + } + + override fun onModesetChange(player: Player) { + val inventory = player.inventory + val offHandItem = inventory.itemInOffHand + + if (isItemBlocked(offHandItem)) { + sendDeniedMessage(player) + inventory.setItemInOffHand(ItemStack(Material.AIR)) + if (!inventory.addItem(offHandItem).isEmpty()) player.world.dropItemNaturally(player.location, offHandItem) + } + } + + private fun isItemBlocked(item: ItemStack?): Boolean { + if (item == null || item.type == Material.AIR) { + return false + } + + return !blockType!!.isAllowed(materials, item.type) + } + + private enum class BlockType(private val filter: BiPredicate?, Material?>) { + WHITELIST(BiPredicate { obj: Collection?, o: Material? -> obj!!.contains(o) }), + BLACKLIST(not { obj: Collection?, o: Material? -> + obj!!.contains( + o + ) + }); + + /** + * Checks whether the given material is allowed. + * + * @param list the list to use for checking + * @param toCheck the material to check + * @return true if the item is allowed, based on the list and the current mode + */ + fun isAllowed(list: Collection?, toCheck: Material?): Boolean { + return filter.test(list, toCheck) + } + } + + companion object { + private const val OFFHAND_SLOT = 40 + private fun not(predicate: BiPredicate): BiPredicate { + return predicate.negate() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleFishingKnockback.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleFishingKnockback.kt new file mode 100644 index 00000000..9a27a7ad --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleFishingKnockback.kt @@ -0,0 +1,147 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.entity.* +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.ProjectileHitEvent +import org.bukkit.event.player.PlayerFishEvent +import org.bukkit.util.Vector +import kotlin.math.sqrt + + +/** + * Brings back the old fishing-rod knockback. + */ +class ModuleFishingKnockback(plugin: OCMMain) : OCMModule(plugin, "old-fishing-knockback") { + private val getHookFunction: SpigotFunctionChooser + private val getHitEntityFunction: SpigotFunctionChooser + private var knockbackNonPlayerEntities = false + + init { + reload() + + getHookFunction = SpigotFunctionChooser.apiCompatReflectionCall( + { e, _ -> e.hook }, PlayerFishEvent::class.java, "getHook" + ) + getHitEntityFunction = SpigotFunctionChooser.apiCompatCall({ e, _ -> e.hitEntity }, { e, _ -> + val hookEntity: Entity = e.entity + val world = hookEntity.world + world.getNearbyEntities(hookEntity.location, 0.25, 0.25, 0.25).stream() + .filter { entity: Entity? -> !knockbackNonPlayerEntities && entity is Player }.findFirst().orElse(null) + }) + } + + override fun reload() { + knockbackNonPlayerEntities = isSettingEnabled("knockbackNonPlayerEntities") + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + fun onRodLand(event: ProjectileHitEvent) { + val hookEntity: Entity = event.entity + val world = hookEntity.world + + // FISHING_HOOK -> FISHING_BOBBER in >=1.20.5 + val fishingBobberType = try { + EntityType.FISHING_BOBBER + } catch (e: NoSuchFieldError) { + EntityType.valueOf("FISHING_HOOK") + } + if (event.entityType != fishingBobberType) return + + val hook = hookEntity as FishHook + + if (hook.shooter !is Player) return + val rodder = hook.shooter as Player + if (!isEnabled(rodder)) return + + val hitEntity = getHitEntityFunction.apply(event) ?: return + + // If no entity was hit + + if (hitEntity !is LivingEntity) return + if (!knockbackNonPlayerEntities && hitEntity !is Player) return + + // Do not move Citizens NPCs + // See https://wiki.citizensnpcs.co/API#Checking_if_an_entity_is_a_Citizens_NPC + if (hitEntity.hasMetadata("NPC")) return + + + if (!knockbackNonPlayerEntities) { + val player = hitEntity as Player + + debug("You were hit by a fishing rod!", player) + + if (player == rodder) return + + if (player.gameMode == GameMode.CREATIVE) return + } + + // Check if cooldown time has elapsed + if (hitEntity.noDamageTicks > hitEntity.maximumNoDamageTicks / 2f) return + + var damage = module().getDouble("damage") + if (damage < 0) damage = 0.0001 + + hitEntity.damage(damage, rodder) + hitEntity.setVelocity( + calculateKnockbackVelocity( + hitEntity.getVelocity(), hitEntity.getLocation(), hook.location + ) + ) + } + + private fun calculateKnockbackVelocity(currentVelocity: Vector, player: Location, hook: Location): Vector { + var xDistance = hook.x - player.x + var zDistance = hook.z - player.z + + // ensure distance is not zero and randomise in that case (I guess?) + while (xDistance * xDistance + zDistance * zDistance < 0.0001) { + xDistance = (Math.random() - Math.random()) * 0.01 + zDistance = (Math.random() - Math.random()) * 0.01 + } + + val distance = sqrt(xDistance * xDistance + zDistance * zDistance) + + var y = currentVelocity.y / 2 + var x = currentVelocity.x / 2 + var z = currentVelocity.z / 2 + + // Normalise distance to have similar knockback, no matter the distance + x -= xDistance / distance * 0.4 + + // slow the fall or throw upwards + y += 0.4 + + // Normalise distance to have similar knockback, no matter the distance + z -= zDistance / distance * 0.4 + + // do not shoot too high up + if (y >= 0.4) y = 0.4 + + return Vector(x, y, z) + } + + /** + * This is to cancel dragging the entity closer when you reel in + */ + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + private fun onReelIn(e: PlayerFishEvent) { + if (e.state != PlayerFishEvent.State.CAUGHT_ENTITY) return + + val cancelDraggingIn = module().getString("cancelDraggingIn", "players")!! + val isPlayer = e.caught is HumanEntity + if ((cancelDraggingIn == "players" && isPlayer) || cancelDraggingIn == "mobs" && !isPlayer || cancelDraggingIn == "all") { + getHookFunction.apply(e)!!.remove() // Remove the bobber and don't do anything else + e.isCancelled = true + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleFishingRodVelocity.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleFishingRodVelocity.kt new file mode 100644 index 00000000..e8c3e92a --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleFishingRodVelocity.kt @@ -0,0 +1,104 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector.versionIsNewerOrEqualTo +import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser +import org.bukkit.Material +import org.bukkit.entity.FishHook +import org.bukkit.event.EventHandler +import org.bukkit.event.player.PlayerFishEvent +import org.bukkit.scheduler.BukkitRunnable +import org.bukkit.util.Vector +import java.util.* +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +/** + * This module reverts fishing rod gravity and velocity back to 1.8 behaviour + * + * + * Fishing rod gravity in 1.14+ is 0.03 while in 1.8 it is 0.04 + * Launch velocity in 1.9+ is also different from the 1.8 formula + */ +class ModuleFishingRodVelocity(plugin: OCMMain) : OCMModule(plugin, "fishing-rod-velocity") { + private var random: Random? = null + private var hasDifferentGravity = false + + // In 1.12- getHook() returns a Fish which extends FishHook + + private val getHook: SpigotFunctionChooser = + SpigotFunctionChooser.apiCompatReflectionCall( + { e, _ -> e.hook }, + PlayerFishEvent::class.java, "getHook" + ) + + init { + reload() + } + + override fun reload() { + random = Random() + + // Versions 1.14+ have different gravity than previous versions + hasDifferentGravity = versionIsNewerOrEqualTo(1, 14, 0) + } + + @EventHandler(ignoreCancelled = true) + fun onFishEvent(event: PlayerFishEvent) { + val fishHook = getHook.apply(event) + val player = event.player + + if (!isEnabled(player) || event.state != PlayerFishEvent.State.FISHING) return + + val location = event.player.location + val playerYaw = location.yaw.toDouble() + val playerPitch = location.pitch.toDouble() + + val oldMaxVelocity = 0.4f + var velocityX = + -sin(playerYaw / 180.0f * Math.PI.toFloat()) * cos(playerPitch / 180.0f * Math.PI.toFloat()) * oldMaxVelocity + var velocityZ = + cos(playerYaw / 180.0f * Math.PI.toFloat()) * cos(playerPitch / 180.0f * Math.PI.toFloat()) * oldMaxVelocity + var velocityY = -sin(playerPitch / 180.0f * Math.PI.toFloat()) * oldMaxVelocity + + val oldVelocityMultiplier = 1.5 + + val vectorLength = + sqrt(velocityX * velocityX + velocityY * velocityY + velocityZ * velocityZ).toFloat().toDouble() + velocityX /= vectorLength + velocityY /= vectorLength + velocityZ /= vectorLength + + velocityX += random!!.nextGaussian() * 0.007499999832361937 + velocityY += random!!.nextGaussian() * 0.007499999832361937 + velocityZ += random!!.nextGaussian() * 0.007499999832361937 + + velocityX *= oldVelocityMultiplier + velocityY *= oldVelocityMultiplier + velocityZ *= oldVelocityMultiplier + + fishHook!!.velocity = Vector(velocityX, velocityY, velocityZ) + + if (!hasDifferentGravity) return + + // Adjust gravity on every tick unless it's in water + object : BukkitRunnable() { + override fun run() { + if (!fishHook.isValid || fishHook.isOnGround) cancel() + + // We check both conditions as sometimes it's underwater but in seagrass, or when bobbing not underwater but the material is water + if (!fishHook.isInWater && fishHook.world.getBlockAt(fishHook.location).type != Material.WATER) { + val fVelocity = fishHook.velocity + fVelocity.setY(fVelocity.y - 0.01) + fishHook.velocity = fVelocity + } + } + }.runTaskTimer(plugin, 1, 1) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleGoldenApple.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleGoldenApple.kt new file mode 100644 index 00000000..f033f4af --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleGoldenApple.kt @@ -0,0 +1,312 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import com.google.common.collect.ImmutableSet +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.send +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat.Companion.fromNewName +import kernitus.plugin.OldCombatMechanics.versions.materials.MaterialRegistry.ENCHANTED_GOLDEN_APPLE +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.entity.LivingEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.inventory.PrepareItemCraftEvent +import org.bukkit.event.player.PlayerItemConsumeEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.ShapedRecipe +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* +import kotlin.math.max + +/** + * Customise the golden apple effects. + */ +class ModuleGoldenApple(plugin: OCMMain) : OCMModule(plugin, "old-golden-apples") { + private var enchantedGoldenAppleEffects: List? = null + private var goldenAppleEffects: List? = null + private var enchantedAppleRecipe: ShapedRecipe? = null + + private var lastEaten: MutableMap? = null + private var cooldown: Cooldown? = null + + private var normalCooldownMessage: String? = null + private var enchantedCooldownMessage: String? = null + + init { + instance = this + } + + override fun reload() { + normalCooldownMessage = module().getString("cooldown.message-normal") + enchantedCooldownMessage = module().getString("cooldown.message-enchanted") + + cooldown = Cooldown( + module().getLong("cooldown.normal"), + module().getLong("cooldown.enchanted"), + module().getBoolean("cooldown.is-shared") + ) + lastEaten = WeakHashMap() + + enchantedGoldenAppleEffects = getPotionEffects("enchanted-golden-apple-effects") + goldenAppleEffects = getPotionEffects("golden-apple-effects") + + enchantedAppleRecipe = try { + ShapedRecipe( + NamespacedKey(plugin, "MINECRAFT"), ENCHANTED_GOLDEN_APPLE.newInstance() + ) + } catch (e: NoClassDefFoundError) { + ShapedRecipe(ENCHANTED_GOLDEN_APPLE.newInstance()) + } + enchantedAppleRecipe!!.shape("ggg", "gag", "ggg").setIngredient('g', Material.GOLD_BLOCK) + .setIngredient('a', Material.APPLE) + + registerCrafting() + } + + private fun registerCrafting() { + if (isEnabled() && module().getBoolean("enchanted-golden-apple-crafting")) { + if (Bukkit.getRecipesFor(ENCHANTED_GOLDEN_APPLE.newInstance()).isNotEmpty()) return + Bukkit.addRecipe(enchantedAppleRecipe) + debug("Added napple recipe") + } + } + + @EventHandler(priority = EventPriority.HIGH) + fun onPrepareItemCraft(e: PrepareItemCraftEvent) { + val item = e.inventory.result ?: return + // This should never ever ever ever run. If it does then you probably screwed something up. + + + if (ENCHANTED_GOLDEN_APPLE.isSame(item)) { + val player = e.view.player + + if (isSettingEnabled("no-conflict-mode")) return + + if (!isEnabled(player) || !isSettingEnabled("enchanted-golden-apple-crafting")) e.inventory.result = null + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun onItemConsume(e: PlayerItemConsumeEvent) { + val player = e.player + + if (!isEnabled(player)) return + + val originalItem = e.item + val consumedMaterial = originalItem.type + + if (consumedMaterial != Material.GOLDEN_APPLE && !ENCHANTED_GOLDEN_APPLE.isSame(originalItem)) return + + val uuid = player.uniqueId + + // Check if the cooldown has expired yet + lastEaten!!.putIfAbsent(uuid, LastEaten()) + + // If on cooldown send appropriate cooldown message + if (cooldown!!.isOnCooldown(originalItem, lastEaten!![uuid]!!)) { + val le = lastEaten!![uuid] + + val baseCooldown: Long + var current: Instant? + val message: String? + + if (consumedMaterial == Material.GOLDEN_APPLE) { + baseCooldown = cooldown!!.normal + current = le!!.lastNormalEaten + message = normalCooldownMessage + } else { + baseCooldown = cooldown!!.enchanted + current = le!!.lastEnchantedEaten + message = enchantedCooldownMessage + } + + val newestEatTime = le.newestEatTime + if (cooldown!!.sharedCooldown && newestEatTime.isPresent) current = newestEatTime.get() + + val seconds = baseCooldown - (Instant.now().epochSecond - current!!.epochSecond) + + if (!message.isNullOrEmpty()) send( + player, message.replace("%seconds%".toRegex(), seconds.toString()) + ) + + e.isCancelled = true + return + } + + lastEaten!![uuid]!!.setForItem(originalItem) + + if (!isSettingEnabled("old-potion-effects")) return + + // Save player's current potion effects + val previousPotionEffects = player.activePotionEffects + + val newEffects = + if (ENCHANTED_GOLDEN_APPLE.isSame(originalItem)) enchantedGoldenAppleEffects else goldenAppleEffects + val defaultEffects = if (ENCHANTED_GOLDEN_APPLE.isSame(originalItem)) nappleEffects else gappleEffects + + Bukkit.getScheduler().runTaskLater(plugin, Runnable { + // Remove all potion effects the apple added + player.activePotionEffects.stream().map { obj: PotionEffect -> obj.type } + .filter { o: PotionEffectType? -> defaultEffects.contains(o) } + .forEach { type: PotionEffectType? -> player.removePotionEffect(type!!) } + // Add previous potion effects from before eating the apple + player.addPotionEffects(previousPotionEffects) + // Add new custom effects from eating the apple + applyEffects(player, newEffects!!) + }, 1L) + } + + + private fun applyEffects(target: LivingEntity, newEffects: List) { + for (newEffect in newEffects) { + // Find the existing effect of the same type with the highest amplifier + val highestExistingEffect = + target.activePotionEffects.stream().filter { e: PotionEffect -> e.type === newEffect.type } + .max(Comparator.comparingInt { obj: PotionEffect -> obj.amplifier }).orElse(null) + + if (highestExistingEffect != null) { + // If the new effect has a higher amplifier, apply it + if (newEffect.amplifier > highestExistingEffect.amplifier) { + target.addPotionEffect(newEffect) + } else if (newEffect.amplifier == highestExistingEffect.amplifier && newEffect.duration > highestExistingEffect.duration) { + target.addPotionEffect(newEffect) + } + // If the new effect has a lower amplifier or shorter/equal duration, do nothing + } else { + // If there is no existing effect of the same type, apply the new effect + target.addPotionEffect(newEffect) + } + } + } + + + private fun getPotionEffects(path: String): List { + val appleEffects: MutableList = ArrayList() + + val sect = module().getConfigurationSection(path) + for (key in sect!!.getKeys(false)) { + val duration = sect.getInt("$key.duration") * 20 // Convert seconds to ticks + val amplifier = sect.getInt("$key.amplifier") + + val type = fromNewName(key) + Objects.requireNonNull(type, String.format("Invalid potion effect type '%s'!", key)) + + val fx = PotionEffect(type!!, duration, amplifier) + appleEffects.add(fx) + } + return appleEffects + } + + @EventHandler + fun onPlayerQuit(e: PlayerQuitEvent) { + val uuid = e.player.uniqueId + if (lastEaten != null) lastEaten!!.remove(uuid) + } + + /** + * Get player's current golden apple cooldown + * + * @param playerUUID The UUID of the player to check the cooldown for. + * @return The remaining cooldown time in seconds, or 0 if there is no cooldown, or it has expired. + */ + fun getGappleCooldown(playerUUID: UUID): Long { + val lastEatenInfo = lastEaten!![playerUUID] + if (lastEatenInfo?.lastNormalEaten != null) { + val timeElapsedSinceEaten = Duration.between(lastEatenInfo.lastNormalEaten, Instant.now()).seconds + val cooldownRemaining = cooldown!!.normal - timeElapsedSinceEaten + return max(cooldownRemaining.toDouble(), 0.0).toLong() // Return 0 if the cooldown has expired + } + return 0 + } + + /** + * Get player's current enchanted golden apple cooldown + * + * @param playerUUID The UUID of the player to check the cooldown for. + * @return The remaining cooldown time in seconds, or 0 if there is no cooldown, or it has expired. + */ + fun getNappleCooldown(playerUUID: UUID): Long { + val lastEatenInfo = lastEaten!![playerUUID] + if (lastEatenInfo?.lastEnchantedEaten != null) { + val timeElapsedSinceEaten = Duration.between(lastEatenInfo.lastEnchantedEaten, Instant.now()).seconds + val cooldownRemaining = cooldown!!.enchanted - timeElapsedSinceEaten + return max(cooldownRemaining.toDouble(), 0.0).toLong() // Return 0 if the cooldown has expired + } + return 0 + } + + private class LastEaten { + var lastNormalEaten: Instant? = null + var lastEnchantedEaten: Instant? = null + + fun getForItem(item: ItemStack): Optional { + return if (ENCHANTED_GOLDEN_APPLE.isSame(item)) Optional.ofNullable(lastEnchantedEaten) + else Optional.ofNullable(lastNormalEaten) + } + + val newestEatTime: Optional + get() { + if (lastEnchantedEaten == null) { + return Optional.ofNullable(lastNormalEaten) + } + if (lastNormalEaten == null) { + return Optional.of(lastEnchantedEaten!!) + } + return Optional.of( + (if (lastNormalEaten!! < lastEnchantedEaten) lastEnchantedEaten else lastNormalEaten)!! + ) + } + + fun setForItem(item: ItemStack) { + if (ENCHANTED_GOLDEN_APPLE.isSame(item)) { + lastEnchantedEaten = Instant.now() + } else { + lastNormalEaten = Instant.now() + } + } + } + + @JvmRecord + private data class Cooldown(val normal: Long, val enchanted: Long, val sharedCooldown: Boolean) { + fun getCooldownForItem(item: ItemStack): Long { + return if (ENCHANTED_GOLDEN_APPLE.isSame(item)) enchanted else normal + } + + fun isOnCooldown(item: ItemStack, lastEaten: LastEaten): Boolean { + return (if (sharedCooldown) lastEaten.newestEatTime else lastEaten.getForItem(item)).map { it: Instant? -> + ChronoUnit.SECONDS.between( + it, Instant.now() + ) + }.map { it: Long -> it < getCooldownForItem(item) }.orElse(false) + } + } + + companion object { + // Default apple effects + // Gapple: absorption I, regen II + private val gappleEffects: Set = ImmutableSet.of( + PotionEffectType.ABSORPTION, PotionEffectType.REGENERATION + ) + + // Napple: absorption IV, regen II, fire resistance I, resistance I + private val nappleEffects: Set = ImmutableSet.of( + PotionEffectType.ABSORPTION, + PotionEffectType.REGENERATION, + PotionEffectType.FIRE_RESISTANCE, + PotionEffectTypeCompat.RESISTANCE.get() + ) + lateinit var instance: ModuleGoldenApple + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleNoLapisEnchantments.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleNoLapisEnchantments.kt new file mode 100644 index 00000000..bfee02e0 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleNoLapisEnchantments.kt @@ -0,0 +1,87 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.versions.materials.MaterialRegistry +import org.bukkit.entity.HumanEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.enchantment.EnchantItemEvent +import org.bukkit.event.inventory.* +import org.bukkit.inventory.EnchantingInventory +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.ItemStack +import org.bukkit.permissions.Permissible + +/** + * Allows enchanting without needing lapis. + */ +class ModuleNoLapisEnchantments(plugin: OCMMain) : OCMModule(plugin, "no-lapis-enchantments") { + private val lapisLazuli = MaterialRegistry.LAPIS_LAZULI + + @EventHandler + fun onEnchant(e: EnchantItemEvent) { + val player = e.enchanter + if (!isEnabled(player)) return + + if (hasNoPermission(player)) return + + val ei = e.inventory as EnchantingInventory //Not checking here because how else would event be fired? + ei.secondary = lapis + } + + @EventHandler + fun onInventoryClick(e: InventoryClickEvent) { + if (!isEnabled(e.whoClicked)) return + + if (e.inventory.type != InventoryType.ENCHANTING) return + + if (hasNoPermission(e.whoClicked)) return + + val item = e.currentItem ?: return + + // prevent taking it out + if (lapisLazuli.isSame(item) && e.rawSlot == 1) { + e.isCancelled = true + } else if (e.cursor != null && lapisLazuli.isSame(e.cursor!!) && e.click == ClickType.DOUBLE_CLICK) { + e.isCancelled = true + } + } + + @EventHandler + fun onInventoryClose(e: InventoryCloseEvent) { + if (!isEnabled(e.player)) return + + val inventory = e.inventory + if (inventory == null || inventory.type != InventoryType.ENCHANTING) return + + // always clear it, so nothing is left over in the table + (inventory as EnchantingInventory).secondary = null + } + + @EventHandler + fun onInventoryOpen(e: InventoryOpenEvent) { + fillUpEnchantingTable(e.player, e.inventory) + } + + private fun fillUpEnchantingTable(player: HumanEntity, inventory: Inventory?) { + if (!isEnabled(player)) return + + if (inventory == null || inventory.type != InventoryType.ENCHANTING || hasNoPermission(player)) return + (inventory as EnchantingInventory).secondary = lapis + } + + private val lapis: ItemStack + get() { + val lapis = lapisLazuli.newInstance() + lapis!!.amount = 64 + return lapis + } + + private fun hasNoPermission(player: Permissible): Boolean { + return isSettingEnabled("usePermission") && !player.hasPermission("oldcombatmechanics.nolapis") + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourDurability.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourDurability.kt new file mode 100644 index 00000000..989019ce --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourDurability.kt @@ -0,0 +1,86 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.entity.EntityType +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import org.bukkit.event.player.PlayerItemDamageEvent +import org.bukkit.inventory.ItemStack +import java.util.* +import java.util.stream.Collectors + +class ModuleOldArmourDurability(plugin: OCMMain) : OCMModule(plugin, "old-armour-durability") { + private val explosionDamaged: MutableMap> = WeakHashMap() + + @EventHandler(priority = EventPriority.LOWEST) + fun onItemDamage(e: PlayerItemDamageEvent) { + val player = e.player + + if (!isEnabled(player)) return + val item = e.item + val itemType = item.type + + // Check if it's a piece of armour they're currently wearing + if (Arrays.stream(player.inventory.armorContents) + .noneMatch { armourPiece: ItemStack? -> armourPiece != null && armourPiece.type == itemType && armourPiece.type != Material.ELYTRA } + ) return + + val uuid = player.uniqueId + if (explosionDamaged.containsKey(uuid)) { + val armour = explosionDamaged[uuid]!! + // ItemStack.equals() checks material, durability and quantity to make sure nothing changed in the meantime + // We're checking all the pieces this way just in case they're wearing two helmets or something strange + val matchedPieces = armour.stream().filter { piece: ItemStack -> piece == item }.toList() + armour.removeAll(matchedPieces) + debug("Item matched explosion, ignoring...", player) + if (!matchedPieces.isEmpty()) return + } + + var reduction = module()!!.getInt("reduction") + + // 60 + (40 / (level + 1) ) % chance that durability is reduced (for each point of durability) + val damageChance = 60 + (40 / (item.getEnchantmentLevel(EnchantmentCompat.UNBREAKING.get()) + 1)) + val random = Random() + val randomInt = random.nextInt(100) // between 0 (inclusive) and 100 (exclusive) + if (randomInt >= damageChance) reduction = 0 + + debug("Item damaged: " + itemType + " Damage: " + e.damage + " Changed to: " + reduction, player) + e.damage = reduction + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onPlayerExplosionDamage(e: EntityDamageEvent) { + if (e.isCancelled) return + if (e.entityType != EntityType.PLAYER) return + val cause = e.cause + if (cause != DamageCause.BLOCK_EXPLOSION && + cause != DamageCause.ENTITY_EXPLOSION + ) return + + val player = e.entity as Player + val uuid = player.uniqueId + val armour = + Arrays.stream(player.inventory.armorContents).filter { obj: ItemStack? -> Objects.nonNull(obj) }.collect( + Collectors.toList() + ) + explosionDamaged[uuid] = armour + + Bukkit.getScheduler().runTaskLater(plugin, Runnable { + explosionDamaged.remove(uuid) + debug("Removed from explosion set!", player) + }, 1L) // This delay seems enough for the durability events to fire + + debug("Detected explosion!", player) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourStrength.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourStrength.kt new file mode 100644 index 00000000..cc35bbf5 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldArmourStrength.kt @@ -0,0 +1,79 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.damage.DefenceUtils.calculateDefenceDamageReduction +import org.bukkit.entity.LivingEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import org.bukkit.event.entity.EntityDamageEvent.DamageModifier +import java.util.* +import java.util.stream.Collectors + +/** + * Reverts the armour strength changes to 1.8 calculations, including enchantments. + * Also recalculates resistance and absorption accordingly. + * + * + * It is based on [this revision](https://minecraft.gamepedia.com/index.php?title=Armor&oldid=909187) + * of the minecraft wiki. + */ +class ModuleOldArmourStrength(plugin: OCMMain) : OCMModule(plugin, "old-armour-strength") { + // Defence order is armour defence points -> resistance -> armour enchants -> absorption + private var randomness = false + + init { + reload() + } + + override fun reload() { + randomness = module().getBoolean("randomness") + } + + @Suppress("deprecation") + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + fun onEntityDamage(e: EntityDamageEvent) { + // 1.8 NMS: Damage = (damage after blocking * (25 - total armour strength)) / 25 + if (e.entity !is LivingEntity) return + + val damagedEntity = e.entity as LivingEntity + + // If there was an attacker, and he does not have this module enabled, return + if (e.cause == DamageCause.ENTITY_ATTACK && e is EntityDamageByEntityEvent) { + val damager = e.damager + if (!isEnabled(damager, damagedEntity)) return + } + + val damageModifiers = + Arrays.stream(DamageModifier.entries.toTypedArray()) + .filter { type: DamageModifier -> e.isApplicable(type) } + .collect( + Collectors.toMap( + { m: DamageModifier -> m }, + { type: DamageModifier? -> e.getDamage(type!!) }) + ) + + calculateDefenceDamageReduction(damagedEntity, damageModifiers, e.cause, randomness) + + // Set the modifiers back to the event + damageModifiers.forEach { (type: DamageModifier, damage: Double) -> e.setDamage(type, damage) } + + debug("BASE: " + damageModifiers[DamageModifier.BASE]) + debug("BLOCKING: " + damageModifiers[DamageModifier.BLOCKING]) + debug("ARMOUR: " + damageModifiers[DamageModifier.ARMOR]) + debug("RESISTANCE: " + damageModifiers[DamageModifier.RESISTANCE]) + debug("ARMOUR ENCHS: " + damageModifiers[DamageModifier.MAGIC]) + debug("ABSORPTION: " + damageModifiers[DamageModifier.ABSORPTION]) + + val finalDamage = + damageModifiers.values.stream().reduce(0.0) { a: Double, b: Double -> java.lang.Double.sum(a, b) } + debug("Final damage after defence calc: $finalDamage") + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldBrewingStand.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldBrewingStand.kt new file mode 100644 index 00000000..5be2066c --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldBrewingStand.kt @@ -0,0 +1,34 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import org.bukkit.block.BrewingStand +import org.bukkit.event.EventHandler +import org.bukkit.event.inventory.InventoryOpenEvent + +/** + * Makes brewing stands not require fuel. + */ +class ModuleOldBrewingStand(plugin: OCMMain) : OCMModule(plugin, "old-brewing-stand") { + @EventHandler + fun onInventoryOpen(e: InventoryOpenEvent) { + // Set max fuel when they open brewing stand + // If they run out, they can just close and open it again + if (!isEnabled(e.player)) return + + val inventory = e.inventory + val location = inventory.location ?: return + + val block = location.block + val blockState = block.state as? BrewingStand ?: return + + val brewingStand = blockState + + brewingStand.fuelLevel = 20 + brewingStand.update() + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldBurnDelay.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldBurnDelay.kt new file mode 100644 index 00000000..757561ab --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldBurnDelay.kt @@ -0,0 +1,37 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import org.bukkit.event.EventHandler +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.event.entity.EntityDamageEvent.DamageCause + +/** + * Bring back old fire burning delay behaviour + */ +class ModuleOldBurnDelay(plugin: OCMMain) : OCMModule(plugin, "old-burn-delay") { + private var fireTicks = 0 + + init { + reload() + } + + override fun reload() { + fireTicks = module()!!.getInt("fire-ticks") + } + + @EventHandler + fun onFireTick(e: EntityDamageEvent) { + if (e.cause == DamageCause.FIRE) { + val entity = e.entity + if (!isEnabled(entity)) return + + entity.fireTicks = fireTicks + debug("Setting fire ticks to $fireTicks", entity) + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldCriticalHits.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldCriticalHits.kt new file mode 100644 index 00000000..4c9165ac --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldCriticalHits.kt @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.damage.OCMEntityDamageByEntityEvent +import org.bukkit.event.EventHandler + +class ModuleOldCriticalHits(plugin: OCMMain) : OCMModule(plugin, "old-critical-hits") { + private var allowSprinting = false + private var multiplier = 0.0 + + init { + reload() + } + + override fun reload() { + allowSprinting = module()!!.getBoolean("allowSprinting", true) + multiplier = module()!!.getDouble("multiplier", 1.5) + } + + @EventHandler + fun onOCMDamage(e: OCMEntityDamageByEntityEvent) { + if (!isEnabled(e.damager, e.damagee)) return + + // In 1.9, a critical hit requires the player not to be sprinting + if (e.was1_8Crit() && (allowSprinting || !e.wasSprinting())) e.criticalMultiplier = multiplier + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldPotionEffects.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldPotionEffects.kt new file mode 100644 index 00000000..dff3915f --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldPotionEffects.kt @@ -0,0 +1,179 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils.loadPotionDurationsList +import kernitus.plugin.OldCombatMechanics.utilities.damage.OCMEntityDamageByEntityEvent +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionDurations +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionTypeCompat +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionTypeCompat.Companion.fromPotionMeta +import org.bukkit.Material +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.block.Action +import org.bukkit.event.block.BlockDispenseEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerItemConsumeEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.PotionMeta +import org.bukkit.potion.PotionData +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.potion.PotionType + +/** + * Allows configurable potion effect durations. + */ +class ModuleOldPotionEffects(plugin: OCMMain) : OCMModule(plugin, "old-potion-effects") { + private var durations: Map? = null + + init { + reload() + } + + override fun reload() { + durations = loadPotionDurationsList(module()) + } + + /** + * Change the duration using values defined in config for drinking potions + */ + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + fun onPlayerDrinksPotion(event: PlayerItemConsumeEvent) { + val player = event.player + if (!isEnabled(player)) return + + val potionItem = event.item + if (potionItem.type != Material.POTION) return + + adjustPotion(potionItem, false) + event.setItem(potionItem) + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun onPotionDispense(event: BlockDispenseEvent) { + if (!isEnabled(event.block.world)) return + + val item = event.item + val material = item.type + + if (material == Material.SPLASH_POTION || material == Material.LINGERING_POTION) adjustPotion(item, true) + } + + // We change the potion on-the-fly just as it's thrown to be able to change the effect + @EventHandler(priority = EventPriority.HIGHEST) + fun onPotionThrow(event: PlayerInteractEvent) { + val player = event.player + if (!isEnabled(player)) return + + val action = event.action + if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) return + + val item = event.item ?: return + + val material = item.type + if (material == Material.SPLASH_POTION || material == Material.LINGERING_POTION) adjustPotion(item, true) + } + + /** + * Sets custom potion duration and effects + * + * @param potionItem The potion item with adjusted duration and effects + */ + private fun adjustPotion(potionItem: ItemStack, splash: Boolean) { + val potionMeta = potionItem.itemMeta as PotionMeta? ?: return + + val potionTypeCompat = fromPotionMeta(potionMeta) + + if (EXCLUDED_POTION_TYPES.contains(potionTypeCompat)) return + + val duration = getPotionDuration(potionTypeCompat, splash) + if (duration == null) { + debug("Potion type " + potionTypeCompat.newName + " not found in config, leaving as is...") + return + } + + var amplifier = if (potionTypeCompat.isStrong) 1 else 0 + + if (potionTypeCompat == PotionTypeCompat("WEAKNESS")) { + // Set level to 0 so that it doesn't prevent the EntityDamageByEntityEvent from being called + // due to damage being lower than 0 as some 1.9 weapons deal less damage + amplifier = -1 + } + + var potionEffects: List + val potionType = potionTypeCompat.type + potionEffects = try { + potionType?.potionEffects?.stream()?.map { obj: PotionEffect -> obj.type }?.toList() + } catch (e: NoSuchMethodError) { + listOf(potionType!!.effectType) + } as List + + for (effectType in potionEffects) { + potionMeta.addCustomEffect(PotionEffect(effectType, duration, amplifier), false) + } + + try { // For >=1.20 + potionMeta.basePotionType = PotionType.WATER + } catch (e: NoSuchMethodError) { + potionMeta.basePotionData = PotionData(PotionType.WATER) + } + + potionItem.setItemMeta(potionMeta) + } + + + @EventHandler(ignoreCancelled = true) + fun onDamageByEntity(event: OCMEntityDamageByEntityEvent) { + val damager = event.damager + if (!isEnabled(damager, event.damagee)) return + + if (event.hasWeakness()) { + event.isWeaknessModifierMultiplier = module().getBoolean("weakness.multiplier") + val newWeaknessModifier = module().getDouble("weakness.modifier") + event.weaknessModifier = newWeaknessModifier + event.weaknessLevel = 1 + debug( + "Old weakness modifier: " + event.weaknessLevel + + " New: " + newWeaknessModifier, damager + ) + } + + val strengthModifier = event.strengthModifier + + if (strengthModifier > 0) { + event.isStrengthModifierMultiplier = module().getBoolean("strength.multiplier") + event.isStrengthModifierAddend = module().getBoolean("strength.addend") + val newStrengthModifier = module().getDouble("strength.modifier") + event.strengthModifier = newStrengthModifier + debug("Old strength modifier: $strengthModifier New: $newStrengthModifier", damager) + } + } + + private fun getPotionDuration(potionTypeCompat: PotionTypeCompat, splash: Boolean): Int? { + val potionDurations = durations!![potionTypeCompat] ?: return null + val duration = if (splash) potionDurations.splash else potionDurations.drinkable + + debug("Potion type: " + potionTypeCompat.newName + " Duration: " + duration + " ticks") + + return duration + } + + companion object { + private val EXCLUDED_POTION_TYPES: Set = mutableSetOf( // Base potions without any effect + PotionTypeCompat("AWKWARD"), + PotionTypeCompat("MUNDANE"), + PotionTypeCompat("THICK"), + PotionTypeCompat("WATER"), // Instant potions with no further effects + PotionTypeCompat("HARMING"), + PotionTypeCompat("STRONG_HARMING"), + PotionTypeCompat("HEALING"), + PotionTypeCompat("STRONG_HEALING"), // This type doesn't exist anymore >1.20.5, is specially handled in compat class + PotionTypeCompat("UNCRAFTABLE") + ) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldToolDamage.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldToolDamage.kt new file mode 100644 index 00000000..56a66ae4 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleOldToolDamage.kt @@ -0,0 +1,82 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils.getNewSharpnessDamage +import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils.getOldSharpnessDamage +import kernitus.plugin.OldCombatMechanics.utilities.damage.NewWeaponDamage +import kernitus.plugin.OldCombatMechanics.utilities.damage.OCMEntityDamageByEntityEvent +import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages +import org.bukkit.Material +import org.bukkit.event.EventHandler +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import java.util.* +import kotlin.math.abs + +/** + * Restores old tool damage. + */ +class ModuleOldToolDamage(plugin: OCMMain) : OCMModule(plugin, "old-tool-damage") { + + @EventHandler(ignoreCancelled = true) + fun onEntityDamaged(event: OCMEntityDamageByEntityEvent) { + val damager = event.damager + if (event.cause == DamageCause.THORNS) return + + if (!isEnabled(damager, event.damagee)) return + + val weapon = event.weapon + val weaponMaterial = weapon!!.type + debug("Weapon material: $weaponMaterial") + + if (!isWeapon(weaponMaterial)) return + + // If damage was not what we expected, ignore it because it's probably a custom weapon or from another plugin + val oldBaseDamage = event.baseDamage + val expectedBaseDamage = NewWeaponDamage.getDamage(weaponMaterial).toDouble() + // We check difference as calculation inaccuracies can make it not match + if (abs(oldBaseDamage - expectedBaseDamage) > 0.0001) { + debug("Expected $expectedBaseDamage got $oldBaseDamage ignoring weapon...") + return + } + + val newWeaponBaseDamage = WeaponDamages.getDamage(weaponMaterial) + if (newWeaponBaseDamage <= 0) { + debug("Unknown tool type: $weaponMaterial", damager) + return + } + + event.baseDamage = newWeaponBaseDamage + Messenger.debug("Old tool damage: $oldBaseDamage New: $newWeaponBaseDamage") + + + // Set sharpness to 1.8 damage value + val sharpnessLevel = event.sharpnessLevel + val newSharpnessDamage = if (module().getBoolean( + "old-sharpness", true + ) + ) getOldSharpnessDamage(sharpnessLevel) else getNewSharpnessDamage(sharpnessLevel) + + debug("Old sharpness damage: " + event.sharpnessDamage + " New: " + newSharpnessDamage, damager) + event.sharpnessDamage = newSharpnessDamage + + // The mob enchantments damage remains the same and is linear, no need to recalculate it + } + + private fun isWeapon(material: Material): Boolean { + return Arrays.stream(WEAPONS).anyMatch { type: String -> isOfType(material, type) } + } + + private fun isOfType(mat: Material, type: String): Boolean { + return mat.toString().endsWith("_" + type.uppercase()) + } + + companion object { + private val WEAPONS = arrayOf("sword", "axe", "pickaxe", "spade", "shovel", "hoe") + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModulePlayerKnockback.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModulePlayerKnockback.kt new file mode 100644 index 00000000..0dfd0f70 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModulePlayerKnockback.kt @@ -0,0 +1,176 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector.versionIsNewerOrEqualTo +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.attribute.Attribute +import org.bukkit.attribute.AttributeModifier +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import org.bukkit.event.entity.EntityDamageEvent.DamageModifier +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.player.PlayerVelocityEvent +import org.bukkit.util.Vector +import java.util.* +import java.util.function.Consumer +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +/** + * Reverts knockback formula to 1.8. + * Also disables netherite knockback resistance. + */ +class ModulePlayerKnockback(plugin: OCMMain) : OCMModule(plugin, "old-player-knockback") { + private var knockbackHorizontal = 0.0 + private var knockbackVertical = 0.0 + private var knockbackVerticalLimit = 0.0 + private var knockbackExtraHorizontal = 0.0 + private var knockbackExtraVertical = 0.0 + private var netheriteKnockbackResistance = false + + private val playerKnockbackHashMap: MutableMap = WeakHashMap() + + init { + reload() + } + + override fun reload() { + knockbackHorizontal = module()!!.getDouble("knockback-horizontal", 0.4) + knockbackVertical = module()!!.getDouble("knockback-vertical", 0.4) + knockbackVerticalLimit = module()!!.getDouble("knockback-vertical-limit", 0.4) + knockbackExtraHorizontal = module()!!.getDouble("knockback-extra-horizontal", 0.5) + knockbackExtraVertical = module()!!.getDouble("knockback-extra-vertical", 0.1) + netheriteKnockbackResistance = + module()!!.getBoolean("enable-knockback-resistance", false) && versionIsNewerOrEqualTo(1, 16, 0) + } + + @EventHandler + fun onPlayerQuit(e: PlayerQuitEvent) { + playerKnockbackHashMap.remove(e.player.uniqueId) + } + + // Vanilla does its own knockback, so we need to set it again. + // priority = lowest because we are ignoring the existing velocity, which could break other plugins + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + fun onPlayerVelocityEvent(event: PlayerVelocityEvent) { + val uuid = event.player.uniqueId + if (!playerKnockbackHashMap.containsKey(uuid)) return + event.velocity = playerKnockbackHashMap[uuid]!! + playerKnockbackHashMap.remove(uuid) + } + + @EventHandler + fun onEntityDamage(event: EntityDamageEvent) { + // Disable netherite kb, the knockback resistance attribute makes the velocity event not be called + val entity = event.entity + if (entity !is Player || netheriteKnockbackResistance) return + val damagee = entity + + // This depends on the attacker's combat mode + if (event.cause == DamageCause.ENTITY_ATTACK + && event is EntityDamageByEntityEvent + ) { + val damager = event.damager + if (!isEnabled(damager)) return + } else { + if (!isEnabled(damagee)) return + } + + val attribute = damagee.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE) + attribute!!.modifiers.forEach(Consumer { modifier: AttributeModifier? -> + attribute.removeModifier( + modifier!! + ) + }) + } + + // Monitor priority because we don't modify anything here, but apply on velocity change event + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun onEntityDamageEntity(event: EntityDamageByEntityEvent) { + val damager = event.damager as? LivingEntity ?: return + val attacker = damager + + val damagee = event.entity as? Player ?: return + val victim = damagee + + if (event.cause != DamageCause.ENTITY_ATTACK) return + if (event.getDamage(DamageModifier.BLOCKING) > 0) return + + if (attacker is HumanEntity) { + if (!isEnabled(attacker)) return + } else if (!isEnabled(victim)) return + + // Figure out base knockback direction + val attackerLocation = attacker.location + val victimLocation = victim.location + var d0 = attackerLocation.x - victimLocation.x + var d1: Double + + d1 = attackerLocation.z - victimLocation.z + while (d0 * d0 + d1 * d1 < 1.0E-4) { + d0 = (Math.random() - Math.random()) * 0.01 + d1 = (Math.random() - Math.random()) * 0.01 + } + + val magnitude = sqrt(d0 * d0 + d1 * d1) + + // Get player knockback before any friction is applied + val playerVelocity = victim.velocity + + // Apply friction, then add base knockback + playerVelocity.setX((playerVelocity.x / 2) - (d0 / magnitude * knockbackHorizontal)) + playerVelocity.setY((playerVelocity.y / 2) + knockbackVertical) + playerVelocity.setZ((playerVelocity.z / 2) - (d1 / magnitude * knockbackHorizontal)) + + // Calculate bonus knockback for sprinting or knockback enchantment levels + val equipment = attacker.equipment + if (equipment != null) { + val heldItem = + if (equipment.itemInMainHand.type == Material.AIR) equipment.itemInOffHand else equipment.itemInMainHand + + var bonusKnockback = heldItem.getEnchantmentLevel(Enchantment.KNOCKBACK) + if (attacker is Player && attacker.isSprinting) ++bonusKnockback + + if (playerVelocity.y > knockbackVerticalLimit) playerVelocity.setY(knockbackVerticalLimit) + + if (bonusKnockback > 0) { // Apply bonus knockback + playerVelocity.add( + Vector( + (-sin((attacker.location.yaw * 3.1415927f / 180.0f).toDouble()) * bonusKnockback.toFloat() * knockbackExtraHorizontal), + knockbackExtraVertical, + cos((attacker.location.yaw * 3.1415927f / 180.0f).toDouble()) * bonusKnockback.toFloat() * knockbackExtraHorizontal + ) + ) + } + } + + if (netheriteKnockbackResistance) { + // Allow netherite to affect the horizontal knockback. Each piece of armour yields 10% resistance + val resistance = 1 - victim.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE)!! + .value + playerVelocity.multiply(Vector(resistance, 1.0, resistance)) + } + + val victimId = victim.uniqueId + + // Knockback is sent immediately in 1.8+, there is no reason to send packets manually + playerKnockbackHashMap[victimId] = playerVelocity + + // Sometimes PlayerVelocityEvent doesn't fire, remove data to not affect later events if that happens + Bukkit.getScheduler().runTaskLater(plugin, Runnable { playerKnockbackHashMap.remove(victimId) }, 1) + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModulePlayerRegen.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModulePlayerRegen.kt new file mode 100644 index 00000000..1e4df261 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModulePlayerRegen.kt @@ -0,0 +1,87 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.MathsHelper.clamp +import org.bukkit.Bukkit +import org.bukkit.attribute.Attribute +import org.bukkit.entity.EntityType +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityRegainHealthEvent +import org.bukkit.event.player.PlayerQuitEvent +import java.util.* + +/** + * Establishes custom health regeneration rules. + * Default values based on 1.8 from [wiki](https://minecraft.gamepedia.com/Hunger?oldid=948685) + */ +class ModulePlayerRegen(plugin: OCMMain) : OCMModule(plugin, "old-player-regen") { + private val healTimes: MutableMap = WeakHashMap() + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun onRegen(e: EntityRegainHealthEvent) { + if (e.entityType != EntityType.PLAYER + || e.regainReason != EntityRegainHealthEvent.RegainReason.SATIATED + ) return + + val p = e.entity as Player + if (!isEnabled(p)) return + + val playerId = p.uniqueId + + // We cancel the regen, but saturation and exhaustion need to be adjusted separately + // Exhaustion is modified in the next tick, and saturation in the tick following that (if exhaustion > 4) + e.isCancelled = true + + // Get exhaustion & saturation values before healing modifies them + val previousExhaustion = p.exhaustion + val previousSaturation = p.saturation + + // Check that it has been at least x seconds since last heal + val currentTime = System.currentTimeMillis() + val hasLastHealTime = healTimes.containsKey(playerId) + val lastHealTime = healTimes.computeIfAbsent(playerId) { id: UUID? -> currentTime } + + debug( + "Exh: " + previousExhaustion + " Sat: " + previousSaturation + " Time: " + (currentTime - lastHealTime), + p + ) + + // If we're skipping this heal, we must fix the exhaustion in the following tick + if (hasLastHealTime && currentTime - lastHealTime <= module()!!.getLong("interval")) { + Bukkit.getScheduler().runTaskLater(plugin, Runnable { p.exhaustion = previousExhaustion }, 1L) + return + } + + val maxHealth = p.getAttribute(Attribute.GENERIC_MAX_HEALTH)!!.value + val playerHealth = p.health + + if (playerHealth < maxHealth) { + p.health = clamp(playerHealth + module()!!.getInt("amount"), 0.0, maxHealth) + healTimes[playerId] = currentTime + } + + // Calculate new exhaustion value, must be between 0 and 4. If above, it will reduce the saturation in the following tick. + val exhaustionToApply = module()!!.getDouble("exhaustion").toFloat() + + Bukkit.getScheduler().runTaskLater(plugin, Runnable { + // We do this in the next tick because bukkit doesn't stop the exhaustion change when cancelling the event + p.exhaustion = previousExhaustion + exhaustionToApply + debug( + "Exh before: " + previousExhaustion + " Now: " + p.exhaustion + + " Sat now: " + previousSaturation, p + ) + }, 1L) + } + + @EventHandler + fun onPlayerQuit(e: PlayerQuitEvent) { + healTimes.remove(e.player.uniqueId) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleProjectileKnockback.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleProjectileKnockback.kt new file mode 100644 index 00000000..aee6308d --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleProjectileKnockback.kt @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import org.bukkit.entity.EntityType +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent.DamageModifier + +/** + * Adds knockback to eggs, snowballs and ender pearls. + */ +class ModuleProjectileKnockback(plugin: OCMMain) : OCMModule(plugin, "projectile-knockback") { + @EventHandler(priority = EventPriority.NORMAL) + fun onEntityHit(e: EntityDamageByEntityEvent) { + if (!isEnabled(e.damager, e.entity)) return + + when (val type = e.damager.type) { + EntityType.SNOWBALL, EntityType.EGG, EntityType.ENDER_PEARL -> if (e.damage == 0.0) { // So we don't override enderpearl fall damage + e.damage = module().getDouble("damage." + type.toString().lowercase()) + if (e.isApplicable(DamageModifier.ABSORPTION)) e.setDamage(DamageModifier.ABSORPTION, 0.0) + } + + else -> {} + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleShieldDamageReduction.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleShieldDamageReduction.kt new file mode 100644 index 00000000..88bcef13 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleShieldDamageReduction.kt @@ -0,0 +1,124 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import org.bukkit.event.entity.EntityDamageEvent.DamageModifier +import org.bukkit.event.player.PlayerItemDamageEvent +import org.bukkit.inventory.ItemStack +import java.util.* +import java.util.stream.Collectors + +/** + * Allows customising the shield damage reduction percentages. + */ +class ModuleShieldDamageReduction(plugin: OCMMain) : OCMModule(plugin, "shield-damage-reduction") { + private var genericDamageReductionAmount = 0 + private var genericDamageReductionPercentage = 0 + private var projectileDamageReductionAmount = 0 + private var projectileDamageReductionPercentage = 0 + private val fullyBlocked: MutableMap> = WeakHashMap() + + init { + reload() + } + + override fun reload() { + genericDamageReductionAmount = module()!!.getInt("generalDamageReductionAmount", 1) + genericDamageReductionPercentage = module()!!.getInt("generalDamageReductionPercentage", 50) + projectileDamageReductionAmount = module()!!.getInt("projectileDamageReductionAmount", 1) + projectileDamageReductionPercentage = module()!!.getInt("projectileDamageReductionPercentage", 50) + } + + @EventHandler(priority = EventPriority.LOWEST) + fun onItemDamage(e: PlayerItemDamageEvent) { + val player = e.player + if (!isEnabled(player)) return + val uuid = player.uniqueId + val item = e.item + + if (fullyBlocked.containsKey(uuid)) { + val armour = fullyBlocked[uuid]!! + // ItemStack.equals() checks material, durability and quantity to make sure nothing changed in the meantime + // We're checking all the pieces this way just in case they're wearing two helmets or something strange + val matchedPieces = + armour.stream().filter { piece: ItemStack -> piece == item }.collect(Collectors.toList()) + armour.removeAll(matchedPieces) + debug("Ignoring armour durability damage due to full block", player) + if (!matchedPieces.isEmpty()) { + e.isCancelled = true + } + } + } + + @EventHandler(priority = EventPriority.LOWEST) + fun onHit(e: EntityDamageByEntityEvent) { + val entity = e.entity as? Player ?: return + + val player = entity + + if (!isEnabled(e.damager, player)) return + + // Blocking is calculated after base and hard hat, and before armour etc. + val baseDamage = e.getDamage(DamageModifier.BASE) + e.getDamage(DamageModifier.HARD_HAT) + if (!shieldBlockedDamage(baseDamage, e.getDamage(DamageModifier.BLOCKING))) return + + val damageReduction = getDamageReduction(baseDamage, e.cause) + e.setDamage(DamageModifier.BLOCKING, -damageReduction) + val currentDamage = baseDamage - damageReduction + + debug("Blocking: $baseDamage - $damageReduction = $currentDamage", player) + debug("Blocking: $baseDamage - $damageReduction = $currentDamage") + + val uuid = player.uniqueId + + if (currentDamage <= 0) { // Make sure armour is not damaged if fully blocked + val armour = + Arrays.stream(player.inventory.armorContents).filter { obj: ItemStack? -> Objects.nonNull(obj) } + .collect( + Collectors.toList() + ) + fullyBlocked[uuid] = armour + + Bukkit.getScheduler().runTaskLater(plugin, Runnable { + fullyBlocked.remove(uuid) + debug("Removed from fully blocked set!", player) + }, 1L) + } + } + + private fun getDamageReduction(damage: Double, damageCause: DamageCause): Double { + // 1.8 NMS code, where f is damage done, to calculate new damage. + // f = (1.0F + f) * 0.5F; + + // We subtract, to calculate damage reduction instead of new damage + + var reduction = + damage - (if (damageCause == DamageCause.PROJECTILE) projectileDamageReductionAmount else genericDamageReductionAmount) + + // Reduce to percentage + reduction *= (if (damageCause == DamageCause.PROJECTILE) projectileDamageReductionPercentage else genericDamageReductionPercentage) / 100.0 + + // Don't reduce by more than the actual damage done + // As far as I can tell this is not checked in 1.8NMS, and if the damage was low enough + // blocking would lead to higher damage. However, this is hardly the desired result. + if (reduction < 0) reduction = 0.0 + + return reduction + } + + private fun shieldBlockedDamage(attackDamage: Double, blockingReduction: Double): Boolean { + // Only reduce damage if they were hit head on, i.e. the shield blocked some of the damage + // This also takes into account damages that are not blocked by shields + return attackDamage > 0 && blockingReduction < 0 + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordBlocking.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordBlocking.kt new file mode 100644 index 00000000..cb1a7be9 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordBlocking.kt @@ -0,0 +1,243 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector.versionIsNewerOrEqualTo +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.block.Action +import org.bukkit.event.block.BlockCanBuildEvent +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.player.* +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.PlayerInventory +import org.bukkit.scheduler.BukkitTask +import java.util.* +import java.util.function.Consumer + +class ModuleSwordBlocking(plugin: OCMMain) : OCMModule(plugin, "sword-blocking") { + // Not using WeakHashMaps here, for extra reliability + private val storedItems: MutableMap = HashMap() + private val correspondingTasks: MutableMap> = HashMap() + private var restoreDelay = 0 + + // Only used <1.13, where BlockCanBuildEvent.getPlayer() is not available + private var lastInteractedBlocks: MutableMap? = null + + init { + if (!versionIsNewerOrEqualTo(1, 13, 0)) { + lastInteractedBlocks = WeakHashMap() + } + } + + override fun reload() { + restoreDelay = module().getInt("restoreDelay", 40) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onBlockPlace(e: BlockCanBuildEvent) { + if (e.isBuildable) return + + val player: Player? + + // If <1.13 get player who last interacted with block + if (lastInteractedBlocks != null) { + val blockLocation = e.block.location + val uuid = lastInteractedBlocks!!.remove(blockLocation) + player = Bukkit.getServer().getPlayer(uuid!!) + } else player = e.player + + if (player == null || !isEnabled(player)) return + + doShieldBlock(player) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onRightClick(e: PlayerInteractEvent) { + val action = e.action + val player = e.player + + if (!isEnabled(player)) return + + if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return + // If they clicked on an interactive block, the 2nd event with the offhand won't fire + // This is also the case if the main hand item was used, e.g. a bow + // TODO right-clicking on a mob also only fires one hand + if (action == Action.RIGHT_CLICK_BLOCK && e.hand == EquipmentSlot.HAND) return + if (e.isBlockInHand) { + if (lastInteractedBlocks != null) { + val clickedBlock = e.clickedBlock + if (clickedBlock != null) lastInteractedBlocks!![clickedBlock.location] = player.uniqueId + } + return // Handle failed block place in separate listener + } + + doShieldBlock(player) + } + + private fun doShieldBlock(player: Player) { + val inventory = player.inventory + + val mainHandItem = inventory.itemInMainHand + val offHandItem = inventory.itemInOffHand + + if (!isHoldingSword(mainHandItem.type)) return + + if (module().getBoolean("use-permission") && + !player.hasPermission("oldcombatmechanics.swordblock") + ) return + + val id = player.uniqueId + + if (!isPlayerBlocking(player)) { + if (hasShield(inventory)) return + debug("Storing $offHandItem", player) + storedItems[id] = offHandItem + + inventory.setItemInOffHand(SHIELD) + // Force an inventory update to avoid ghost items + player.updateInventory() + } + scheduleRestore(player) + } + + @EventHandler + fun onHotBarChange(e: PlayerItemHeldEvent) { + restore(e.player, true) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onWorldChange(e: PlayerChangedWorldEvent) { + restore(e.player, true) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPlayerLogout(e: PlayerQuitEvent) { + restore(e.player, true) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPlayerDeath(e: PlayerDeathEvent) { + val p = e.entity + val id = p.uniqueId + if (!areItemsStored(id)) return + + //TODO what if they legitimately had a shield? + e.drops.replaceAll { item: ItemStack -> if (item.type == Material.SHIELD) storedItems.remove(id) else item } + + // Handle keepInventory = true + restore(p, true) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPlayerSwapHandItems(e: PlayerSwapHandItemsEvent) { + if (areItemsStored(e.player.uniqueId)) e.isCancelled = true + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onInventoryClick(e: InventoryClickEvent) { + if (e.whoClicked is Player) { + val player = e.whoClicked as Player + + if (areItemsStored(player.uniqueId)) { + val cursor = e.cursor + val current = e.currentItem + if (cursor != null && cursor.type == Material.SHIELD || + current != null && current.type == Material.SHIELD + ) { + e.isCancelled = true + restore(player, true) + } + } + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onItemDrop(e: PlayerDropItemEvent) { + val `is` = e.itemDrop + val p = e.player + + if (areItemsStored(p.uniqueId) && `is`.itemStack.type == Material.SHIELD) { + e.isCancelled = true + restore(p) + } + } + + private fun restore(player: Player) { + restore(player, false) + } + + private fun restore(p: Player, force: Boolean) { + val id = p.uniqueId + + tryCancelTask(id) + + if (!areItemsStored(id)) return + + // If they are still blocking with the shield, postpone restoring + if (!force && isPlayerBlocking(p)) scheduleRestore(p) + else p.inventory.setItemInOffHand(storedItems.remove(id)) + } + + private fun tryCancelTask(id: UUID) { + Optional.ofNullable(correspondingTasks.remove(id)) + .ifPresent { tasks: Collection -> + tasks.forEach( + Consumer { obj: BukkitTask -> obj.cancel() }) + } + } + + private fun scheduleRestore(p: Player) { + val id = p.uniqueId + tryCancelTask(id) + + val removeItem = Bukkit.getScheduler() + .runTaskLater(plugin, Runnable { restore(p) }, restoreDelay.toLong()) + + val checkBlocking = Bukkit.getScheduler() + .runTaskTimer(plugin, Runnable { + if (!isPlayerBlocking(p)) restore(p) + }, 10L, 2L) + + val tasks: MutableList = ArrayList(2) + tasks.add(removeItem) + tasks.add(checkBlocking) + correspondingTasks[p.uniqueId] = tasks + } + + private fun areItemsStored(uuid: UUID): Boolean { + return storedItems.containsKey(uuid) + } + + /** + * Checks whether player is blocking or they have just begun to and shield is not fully up yet. + */ + private fun isPlayerBlocking(player: Player): Boolean { + return player.isBlocking || + (versionIsNewerOrEqualTo(1, 11, 0) && player.isHandRaised + && hasShield(player.inventory) + ) + } + + private fun hasShield(inventory: PlayerInventory): Boolean { + return inventory.itemInOffHand.type == Material.SHIELD + } + + private fun isHoldingSword(mat: Material): Boolean { + return mat.toString().endsWith("_SWORD") + } + + companion object { + private val SHIELD = ItemStack(Material.SHIELD) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweep.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweep.kt new file mode 100644 index 00000000..4d322f8a --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweep.kt @@ -0,0 +1,100 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.damage.NewWeaponDamage +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import org.bukkit.inventory.ItemStack +import org.bukkit.scheduler.BukkitTask + +/** + * A module to disable the sweep attack. + */ +class ModuleSwordSweep(plugin: OCMMain) : OCMModule(plugin, "disable-sword-sweep") { + private val sweepLocations: MutableList = ArrayList() + private var sweepDamageCause: DamageCause? = null + private var task: BukkitTask? = null + + init { + sweepDamageCause = try { + // Available from 1.11 onwards + DamageCause.valueOf("ENTITY_SWEEP_ATTACK") + } catch (e: IllegalArgumentException) { + null + } + + reload() + } + + override fun reload() { + // we didn't set anything up in the first place + if (sweepDamageCause != null) return + + if (task != null) task!!.cancel() + + task = Bukkit.getScheduler().runTaskTimer(plugin, Runnable { sweepLocations.clear() }, 0, 1) + } + + + //Changed from HIGHEST to LOWEST to support DamageIndicator plugin + @EventHandler(priority = EventPriority.LOWEST) + fun onEntityDamaged(e: EntityDamageByEntityEvent) { + val damager = e.damager as? Player ?: return + + if (!isEnabled(damager, e.entity)) return + + if (sweepDamageCause != null) { + if (e.cause == sweepDamageCause) { + e.isCancelled = true + debug("Sweep cancelled", damager) + } + // sweep attack detected or not, we do not need to fall back to the guessing implementation + return + } + + val attacker = e.damager as Player + val weapon = attacker.inventory.itemInMainHand + + if (isHoldingSword(weapon.type)) onSwordAttack(e, attacker, weapon) + } + + private fun onSwordAttack(e: EntityDamageByEntityEvent, attacker: Player, weapon: ItemStack) { + //Disable sword sweep + val attackerLocation = attacker.location + + var level = 0 + + try { //In a try catch for servers that haven't updated + level = weapon.getEnchantmentLevel(Enchantment.SWEEPING_EDGE) + } catch (ignored: NoSuchFieldError) { + } + + val damage = NewWeaponDamage.getDamage(weapon.type) * level / (level + 1) + 1 + + if (e.damage == damage.toDouble()) { + // Possibly a sword-sweep attack + if (sweepLocations.contains(attackerLocation)) { + debug("Cancelling sweep...", attacker) + e.isCancelled = true + } + } else { + sweepLocations.add(attackerLocation) + } + } + + private fun isHoldingSword(mat: Material): Boolean { + return mat.toString().endsWith("_SWORD") + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweepParticles.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweepParticles.kt new file mode 100644 index 00000000..d1fbf660 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/ModuleSwordSweepParticles.kt @@ -0,0 +1,72 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import com.comphenix.protocol.PacketType +import com.comphenix.protocol.events.PacketAdapter +import com.comphenix.protocol.events.PacketEvent +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.warn +import org.bukkit.plugin.Plugin + +/** + * A module to disable the sweep attack. + */ +class ModuleSwordSweepParticles(plugin: OCMMain) : OCMModule(plugin, "disable-sword-sweep-particles") { + private val protocolManager = plugin.protocolManager + private val particleListener = ParticleListener(plugin) + + init { + reload() + } + + override fun reload() { + if (isEnabled()) protocolManager!!.addPacketListener(particleListener) + else protocolManager!!.removePacketListener(particleListener) + } + + /** + * Hides sweep particles. + */ + private inner class ParticleListener(plugin: Plugin?) : + PacketAdapter(plugin, PacketType.Play.Server.WORLD_PARTICLES) { + private var disabledDueToError = false + + override fun onPacketSending(packetEvent: PacketEvent) { + if (disabledDueToError || !isEnabled(packetEvent.player.world)) return + + try { + val packetContainer = packetEvent.packet + var particleName = try { + packetContainer.newParticles.read(0).particle.name + } catch (exception: Exception) { + packetContainer.particles.read(0).name // for pre 1.13 + } + + if (particleName.uppercase().contains("SWEEP")) { + packetEvent.isCancelled = true + debug("Cancelled sweep particles", packetEvent.player) + } + } catch (e: Exception) { + disabledDueToError = true + warn( + e, + "Error detecting sweep packets. Please report it along with the following exception " + + "on github." + + "Sweep cancellation should still work, but particles might show up." + ) + } catch (e: ExceptionInInitializerError) { + disabledDueToError = true + warn( + e, + "Error detecting sweep packets. Please report it along with the following exception " + + "on github." + + "Sweep cancellation should still work, but particles might show up." + ) + } + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/OCMModule.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/OCMModule.kt new file mode 100644 index 00000000..76155b1d --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/module/OCMModule.kt @@ -0,0 +1,152 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.module + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.Config.debugEnabled +import kernitus.plugin.OldCombatMechanics.utilities.Config.getModesets +import kernitus.plugin.OldCombatMechanics.utilities.Config.moduleEnabled +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.sendNoPrefix +import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.getPlayerData +import org.bukkit.World +import org.bukkit.command.CommandSender +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.entity.Entity +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.Player +import org.bukkit.event.Listener +import java.util.* + +/** + * A module providing some specific functionality, e.g. restoring fishing rod knockback. + */ +abstract class OCMModule protected constructor(protected var plugin: OCMMain, val configName: String) : Listener { + /** + * Get the module's name, as taken from the class name + * + * @return The module name, e.g. ModuleDisableAttackCooldown + */ + val moduleName: String = javaClass.simpleName + + /** + * Checks whether this module is globally en/disabled. + * + * @return true if this module is globally enabled + */ + open fun isEnabled() = moduleEnabled(configName, null) + + /** + * Checks whether the module is present in the default modeset for the specified world + * + * @param world The world to get the default modeset for + * @return Whether the module is enabled for the found modeset in the given world + */ + open fun isEnabled(world: World) = moduleEnabled(configName, world) + + /** + * Whether this module should be enabled for this player given his current modeset + */ + fun isEnabled(humanEntity: HumanEntity): Boolean { + val world = humanEntity.world + val modesetName = getPlayerData(humanEntity.uniqueId).getModesetForWorld(world.uid) + + if (modesetName == null) { + debug("No modeset found!", humanEntity) + debug("No modeset found for " + humanEntity.name) + return isEnabled(world) + } + + // Check if the modeset contains this module's name + val modeset = getModesets()[modesetName] + return modeset != null && modeset.contains(configName) + } + + fun isEnabled(entity: Entity): Boolean { + if (entity is HumanEntity) return isEnabled(entity) + return isEnabled(entity.world) + } + + /** + * Returns if module should be enabled, giving priority to the attacker, if a human. + * If neither entity is a human, checks if module should be enabled in the defender's world. + * + * @param attacker The entity that is performing the attack + * @param defender The entity that is being attacked + * @return Whether the module should be enabled for this particular interaction + */ + fun isEnabled(attacker: Entity, defender: Entity): Boolean { + if (attacker is HumanEntity) return isEnabled(attacker) + if (defender is HumanEntity) return isEnabled(defender) + return isEnabled(defender.world) + } + + /** + * Checks whether a given setting for this module is enabled. + * + * @param name the name of the setting + * @return true if the setting with that name is enabled. Returns false if the setting did not exist. + */ + fun isSettingEnabled(name: String) = plugin.config.getBoolean("$configName.$name", false) + + /** + * Returns the configuration section for this module. + * + * @return the configuration section for this module + */ + fun module(): ConfigurationSection { + val section = plugin.config.getConfigurationSection(configName) + ?: throw IllegalStateException("Module $configName has no configuration section!") + return section + } + + /** + * Called when the plugin is reloaded. Should re-read all relevant config keys and other resources that might have + * changed. + */ + open fun reload() { + // Intentionally left blank! Meant for individual modules to use. + } + + /** + * Called when player changes modeset. Re-apply any more permanent changes + * depending on result of isEnabled(player). + * + * @param player The player that changed modeset + */ + open fun onModesetChange(player: Player) { + // Intentionally left blank! Meant for individual modules to use. + } + + /** + * Outputs a debug message. + * + * @param text the message text + */ + protected fun debug(text: String) = Messenger.debug("[$moduleName] $text") + + /** + * Sends a debug message to the given command sender. + * + * @param text the message text + * @param sender the sender to send it to + */ + protected fun debug(text: String, sender: CommandSender) { + if (debugEnabled()) { + sendNoPrefix( + sender, + "&8&l[&fDEBUG&8&l][&f$moduleName&8&l]&7 $text" + ) + } + } + + override fun toString(): String { + return Arrays.stream(configName.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .map { word: String -> word[0].uppercaseChar().toString() + word.substring(1).lowercase() } + .reduce { a: String, b: String -> "$a $b" } + .orElse(configName) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/FakePlayer.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/FakePlayer.kt new file mode 100644 index 00000000..b4523469 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/FakePlayer.kt @@ -0,0 +1,174 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.tester + +import com.mojang.authlib.GameProfile +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector +import org.bukkit.Location +import org.bukkit.entity.Entity +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.* + +/* + Resources + + - FakePlayers plugin (1.19 version, had some wrong method names to fix and some things not re-implemented): + https://github.com/gabrianrchua/Fake-Players-Spigot-Plugin/blob/b88b4c33dcf73d9a9a36f6a202b5273712055751/src/main/java/me/KP56/FakePlayers/MultiVersion/v1_19_R1.java + + - Guide on making NMS bots, for a few small tweaks + https://www.spigotmc.org/threads/nms-serverplayer-entityplayer-for-the-1-17-1-18-mojang-mappings-with-fall-damage-and-knockback.551281/ + + - NMS mappings for checking Mojang / Spigot / Obfuscated class, field, and method names + https://mappings.cephx.dev/ + */ +class FakePlayer { + val uuid: UUID = UUID.randomUUID() + val name: String = uuid.toString().substring(0, 16) + private var entityPlayer: ServerPlayer? = null + private var bukkitPlayer: Player? = null + private var tickTaskId: Int? = null + + val serverPlayer: ServerPlayer? + get() = entityPlayer + + fun spawn(location: Location) { + val worldServer: ServerLevel = (location.world as CraftWorld).getHandle() + val mcServer: MinecraftServer = (Bukkit.getServer() as CraftServer).getServer() + + val gameProfile: GameProfile = GameProfile(uuid, name) + this.entityPlayer = ServerPlayer(mcServer, worldServer, gameProfile, null) + + entityPlayer.connection = + ServerGamePacketListenerImpl(mcServer, Connection(PacketFlow.CLIENTBOUND), entityPlayer) + entityPlayer.connection.connection.channel = EmbeddedChannel(ChannelInboundHandlerAdapter()) + entityPlayer.connection.connection.channel.close() + + entityPlayer.setGameMode(GameType.SURVIVAL) + + entityPlayer.setPos(location.x, location.y, location.z) + entityPlayer.setXRot(0) + entityPlayer.setYRot(0) + + try { + val ipAddress = InetAddress.getByName("127.0.0.1") + val asyncPreLoginEvent: AsyncPlayerPreLoginEvent = AsyncPlayerPreLoginEvent(name, ipAddress, uuid) + Thread { Bukkit.getPluginManager().callEvent(asyncPreLoginEvent) }.start() + } catch (e: UnknownHostException) { + e.printStackTrace() + } + + // TODO playerloginevent might need to get called separately + //PlayerLoginEvent playerLoginEvent = new PlayerLoginEvent((Player) entityPlayer, "hostname", ipAddress, ipAddress); + val playerList: PlayerList = mcServer.getPlayerList() + playerList.load(entityPlayer) + entityPlayer.spawnIn(worldServer) + playerList.getPlayers().add(entityPlayer) + + // Get private playerByUUID Map from PlayerList class and add player to it + // private final Map playersByUUID = Maps.newHashMap(); + val playersByUUIDField = Reflector.getMapFieldWithTypes( + PlayerList::class.java, + UUID::class.java, + ServerPlayer::class.java + ) + val playerByUUID: MutableMap = + Reflector.getFieldValue(playersByUUIDField, playerList) as MutableMap + playerByUUID[uuid] = entityPlayer + + bukkitPlayer = Bukkit.getPlayer(uuid) + + if (bukkitPlayer == null) throw RuntimeException("Bukkit player with UUID $uuid not found!") + + val joinMessage = "§e" + entityPlayer.displayName + " joined the game" + val playerJoinEvent: PlayerJoinEvent = + PlayerJoinEvent(bukkitPlayer, net.kyori.adventure.text.Component.text(joinMessage)) + Bukkit.getPluginManager().callEvent(playerJoinEvent) + + // Let other client know player is there + sendPacket(ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.ADD_PLAYER, entityPlayer)) + + worldServer.addNewPlayer(entityPlayer) + + // Send world info to player client + playerList.sendLevelInfo(entityPlayer, worldServer) + entityPlayer.sendServerStatus(playerList.getServer().getStatus()) + + // Spawn the player for the client + sendPacket(ClientboundAddPlayerPacket(entityPlayer)) + + tickTaskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(OCMMain.getInstance(), entityPlayer::tick, 1, 1) + } + + fun removePlayer() { + if (tickTaskId != null) Bukkit.getScheduler().cancelTask(tickTaskId) + tickTaskId = null + + val mcServer: MinecraftServer = (Bukkit.getServer() as CraftServer).getServer() + + val quitMessage: net.kyori.adventure.text.Component = + net.kyori.adventure.text.Component.text("§e" + entityPlayer.displayName + " left the game") + val playerQuitEvent: PlayerQuitEvent = + PlayerQuitEvent(bukkitPlayer, quitMessage, PlayerQuitEvent.QuitReason.DISCONNECTED) + Bukkit.getPluginManager().callEvent(playerQuitEvent) + + entityPlayer.getBukkitEntity().disconnect(quitMessage.toString()) + + val playerList: PlayerList = mcServer.getPlayerList() + playerList.remove(entityPlayer) + } + + fun attack(bukkitEntity: Entity) { + bukkitPlayer.attack(bukkitEntity) + } + + fun updateEquipment(slot: EquipmentSlot?, item: ItemStack?) { + //todo try directly accessing the player inventory + // otherwise just set the attack value attribute instead + // could check Citizen's code to see how they give weapons + // might also just need to wait a tick + + //final ServerLevel worldServer = entityPlayer.x(); // entityPlayer.getWorld().getWorld().getHandle(); + // worldServer.broadcastEntityEvent(Entity, byte) + + //Defining the list of Pairs with EquipmentSlot and (NMS) ItemStack + + val equipmentList: MutableList> = ArrayList>() + equipmentList.add(Pair(slot, CraftItemStack.asNMSCopy(item))) + + //Creating the packet + val entityEquipment: ClientboundSetEquipmentPacket = + ClientboundSetEquipmentPacket(bukkitPlayer.getEntityId(), equipmentList) + sendPacket(entityEquipment) + // ((ServerLevel) this.level).getChunkSource().broadcast(this, new PacketPlayOutEntityEquipment(this.getId(), list)); + } + + private fun sendPacket(packet: Packet) { + Bukkit.getOnlinePlayers().stream() + .map { p: Player? -> (p as CraftPlayer).getHandle().connection } + .forEach { connection: Any -> connection.send(packet) } + } + + /** + * Make this player block with a shield + */ + fun doBlocking() { + bukkitPlayer.getInventory().setItemInMainHand(ItemStack(Material.SHIELD)) + + val entityLiving: LivingEntity = (bukkitPlayer as CraftLivingEntity).getHandle() + entityLiving.startUsingItem(InteractionHand.MAIN_HAND) + // getUseDuration of SHIELD is 72000 + // For isBlocking to be true, useDuration - getUseItemRemainingTicks() must be >= 5 + // Which means we have to wait at least 5 ticks before user is actually blocking + // Here we just set it manually + val reflectionRemapper: ReflectionRemapper = ReflectionRemapper.forReobfMappingsInPaperJar() + val remapped: String = reflectionRemapper.remapFieldName(LivingEntity::class.java, "useItemRemaining") + val useItemRemainingField = Reflector.getField( + LivingEntity::class.java, remapped + ) + Reflector.setFieldValue(useItemRemainingField, entityLiving, 200) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/InGameTester.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/InGameTester.kt new file mode 100644 index 00000000..df3d3cff --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/InGameTester.kt @@ -0,0 +1,435 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.tester + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.tester.TesterUtils.assertEquals +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.debug +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.send +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.sendNoPrefix +import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils.getOldSharpnessDamage +import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils.isCriticalHit1_8 +import kernitus.plugin.OldCombatMechanics.utilities.damage.DefenceUtils.getDamageAfterArmour1_8 +import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages.getDamage +import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages.materialDamages +import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.getPlayerData +import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.setPlayerData +import org.bukkit.Bukkit +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.attribute.Attribute +import org.bukkit.attribute.AttributeModifier +import org.bukkit.command.CommandSender +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import java.util.* +import java.util.function.Consumer +import kotlin.math.max + +class InGameTester(private val ocm: OCMMain) { + private var tally: Tally? = null + private var sender: CommandSender? = null + private var attacker: Player? = null + private var defender: Player? = null + private var fakeAttacker: FakePlayer? = null + private var fakeDefender: FakePlayer? = null + private val testQueue: Queue = ArrayDeque() + + /** + * Perform all tests using the two specified players + */ + fun performTests(sender: CommandSender?, location: Location) { + this.sender = sender + fakeAttacker = FakePlayer() + fakeAttacker!!.spawn(location.add(2.0, 0.0, 0.0)) + fakeDefender = FakePlayer() + val defenderLocation = location.add(0.0, 0.0, 2.0) + fakeDefender!!.spawn(defenderLocation) + + attacker = Bukkit.getPlayer(fakeAttacker!!.uuid) + defender = Bukkit.getPlayer(fakeDefender!!.uuid) + + // Turn defender to face attacker + defenderLocation.yaw = 180f + defenderLocation.pitch = 0f + defender!!.teleport(defenderLocation) + + // modeset of attacker takes precedence + var playerData = getPlayerData(attacker!!.uniqueId) + playerData.setModesetForWorld(attacker!!.world.uid, "old") + setPlayerData(attacker!!.uniqueId, playerData) + + playerData = getPlayerData(defender!!.uniqueId) + playerData.setModesetForWorld(defender!!.world.uid, "new") + setPlayerData(defender!!.uniqueId, playerData) + + beforeAll() + tally = Tally() + + // Queue all tests + //runAttacks(new ItemStack[]{}, () -> {}); // with no armour + testArmour() + + //testEnchantedMelee(new ItemStack[]{}, () -> {}); + + // Run all tests in the queue + runQueuedTests() + } + + private fun runAttacks(armour: Array, preparations: Runnable) { + //testMelee(armour, preparations); + testEnchantedMelee(armour, preparations) + testOverdamage(armour, preparations) + } + + private fun testArmour() { + val materials = arrayOf("LEATHER", "CHAINMAIL", "GOLDEN", "IRON", "DIAMOND", "NETHERITE") + val slots = arrayOf("BOOTS", "LEGGINGS", "CHESTPLATE", "HELMET") + val random = Random(System.currentTimeMillis()) + + val armourContents = arrayOfNulls(4) + for (i in slots.indices) { + val slot = slots[i] + // Pick a random material for each slot + val material = materials[random.nextInt(6)] + + val itemStack = ItemStack(Material.valueOf(material + "_" + slot)) + + // Apply enchantment to armour piece + itemStack.addUnsafeEnchantment(Enchantment.PROTECTION_ENVIRONMENTAL, 50) + + armourContents[i] = itemStack + } + + runAttacks(armourContents) { + defender!!.inventory.armorContents = armourContents + // Test status effects on defence: resistance, fire resistance, absorption + defender!!.addPotionEffect(PotionEffect(PotionEffectType.DAMAGE_RESISTANCE, 10, 1)) + fakeDefender!!.doBlocking() + } + } + + private fun testEnchantedMelee(armour: Array, preparations: Runnable) { + for (weaponType in materialDamages.keys) { + val weapon = ItemStack(weaponType) + + // only axe and sword can have sharpness + try { + weapon.addEnchantment(Enchantment.DAMAGE_ALL, 3) + } catch (ignored: IllegalArgumentException) { + } + + val message = weaponType.name + " Sharpness 3" + queueAttack(OCMTest(weapon, armour, 2, message, Runnable { + preparations.run() + defender!!.maximumNoDamageTicks = 0 + attacker!!.addPotionEffect(PotionEffect(PotionEffectType.INCREASE_DAMAGE, 10, 0, false)) + attacker!!.addPotionEffect(PotionEffect(PotionEffectType.WEAKNESS, 10, -1, false)) + debug("TESTING WEAPON $weaponType") + attacker!!.fallDistance = 2f // Crit + })) + } + } + + private fun testMelee(armour: Array, preparations: Runnable) { + for (weaponType in materialDamages.keys) { + val weapon = ItemStack(weaponType) + queueAttack(OCMTest(weapon, armour, 1, weaponType.name) { + preparations.run() + defender!!.maximumNoDamageTicks = 0 + }) + } + } + + private fun testOverdamage(armour: Array, preparations: Runnable) { + // 1, 5, 6, 7, 3, 8 according to OCM + // 1, 4, 5, 6, 2, 7 according to 1.9+ + val weapons = arrayOf( + Material.WOODEN_HOE, + Material.WOODEN_SWORD, + Material.STONE_SWORD, + Material.IRON_SWORD, + Material.WOODEN_PICKAXE, + Material.DIAMOND_SWORD + ) + + for (weaponType in weapons) { + val weapon = ItemStack(weaponType) + queueAttack(OCMTest(weapon, armour, 3, weaponType.name, Runnable { + preparations.run() + defender!!.maximumNoDamageTicks = 30 + })) + } + } + + private fun queueAttack(test: OCMTest) { + testQueue.add(test) + } + + private fun calculateAttackDamage(weapon: ItemStack): Double { + val weaponType = weapon.type + // Attack components order: (Base + Potion effects, scaled by attack delay) + Critical Hit + (Enchantments, scaled by attack delay) + // Hurt components order: Overdamage - Armour Effects + var expectedDamage = getDamage(weaponType) + + // Weakness effect, 1.8: -0.5 + // We ignore the level as there is only one level of weakness potion + val weaknessAddend = if (attacker!!.hasPotionEffect(PotionEffectType.WEAKNESS)) -0.5 else 0.0 + + // Strength effect + // 1.8: +130% for each strength level + val strength = attacker!!.getPotionEffect(PotionEffectType.INCREASE_DAMAGE) + if (strength != null) expectedDamage += (strength.amplifier + 1) * 1.3 * expectedDamage + + expectedDamage += weaknessAddend + + // Take into account damage reduction because of cooldown + val attackCooldown = defender!!.attackCooldown + expectedDamage *= (0.2f + attackCooldown * attackCooldown * 0.8f).toDouble() + + // Critical hit + if (isCriticalHit1_8(attacker!!)) { + expectedDamage *= 1.5 + } + + // Weapon Enchantments + var sharpnessDamage = getOldSharpnessDamage(weapon.getEnchantmentLevel(Enchantment.DAMAGE_ALL)) + sharpnessDamage *= attackCooldown.toDouble() // Scale by attack cooldown strength + expectedDamage += sharpnessDamage + + return expectedDamage + } + + private fun wasFakeOverdamage(weapon: ItemStack): Boolean { + val weaponDamage = calculateAttackDamage(weapon) + val lastDamage = defender!!.lastDamage + return defender!!.noDamageTicks.toFloat() > defender!!.maximumNoDamageTicks.toFloat() / 2.0f && + weaponDamage <= lastDamage + } + + private fun wasOverdamaged(rawWeaponDamage: Double): Boolean { + val lastDamage = defender!!.lastDamage + return defender!!.noDamageTicks.toFloat() > defender!!.maximumNoDamageTicks.toFloat() / 2.0f && + rawWeaponDamage > lastDamage + } + + private fun calculateExpectedDamage(weapon: ItemStack, armourContents: Array): Float { + var expectedDamage = calculateAttackDamage(weapon) + + // Overdamage + if (wasOverdamaged(expectedDamage)) { + val lastDamage = defender!!.lastDamage + send( + sender!!, + "Overdamaged: " + expectedDamage + " - " + lastDamage + " = " + (expectedDamage - lastDamage) + ) + debug("Overdamaged: " + expectedDamage + " - " + lastDamage + " = " + (expectedDamage - lastDamage)) + expectedDamage -= lastDamage + } + + // BASE -> HARD_HAT -> BLOCKING -> ARMOUR -> RESISTANCE -> MAGIC -> ABSORPTION + + // Blocking + //1.8 default: (damage - 1) * 50% 1.9 default: 33% 1.11 default: 100% + if (defender!!.isBlocking) { + debug("DEFENDER IS BLOCKING $expectedDamage") + //expectedDamage = (1.0F + expectedDamage) * 0.5F; + expectedDamage -= max(0.0, (expectedDamage - 1)) * 0.5 + debug("AFTER BLOCC $expectedDamage") + } + + // Armour, resistance, armour enchants (1.8, with OldArmourStrength module) + expectedDamage = getDamageAfterArmour1_8( + defender!!, + expectedDamage, + armourContents, + EntityDamageEvent.DamageCause.ENTITY_ATTACK, + false + ) + + /* 1.8 NMS + float f1 = f; + f = Math.max(f - this.getAbsorptionHearts(), 0.0F); + this.setAbsorptionHearts(this.getAbsorptionHearts() - (f1 - f)); + if (f != 0.0F) { + this.applyExhaustion(damagesource.getExhaustionCost()); + float f2 = this.getHealth(); + + this.setHealth(this.getHealth() - f); + this.bs().a(damagesource, f2, f); + if (f < 3.4028235E37F) { + this.a(StatisticList.x, Math.round(f * 10.0F)); + } + } + */ + return expectedDamage.toFloat() + } + + private fun runQueuedTests() { + send(sender!!, "Running " + testQueue.size + " tests") + + // Listener gets called every time defender is damaged + val listener: Listener = object : Listener { + @EventHandler(priority = EventPriority.MONITOR) + fun onEvent(e: EntityDamageByEntityEvent) { + val damager = e.damager + if (damager.uniqueId !== attacker!!.uniqueId || + e.entity.uniqueId !== defender!!.uniqueId + ) return + + val weapon = (damager as Player).inventory.itemInMainHand + val weaponType = weapon.type + var test = testQueue.remove() + var expectedWeapon = test.weapon + var expectedDamage = calculateExpectedDamage(expectedWeapon, test.armour) + + while (weaponType != expectedWeapon.type) { // One of the attacks dealt no damage + expectedDamage = calculateExpectedDamage(expectedWeapon, test.armour) + send(sender!!, "&bSKIPPED &f" + expectedWeapon.type + " &fExpected Damage: &b" + expectedDamage) + if (expectedDamage == 0f) tally!!.passed() + else tally!!.failed() + test = testQueue.remove() + expectedWeapon = test.weapon + } + + + if (wasFakeOverdamage(weapon) && e.isCancelled) { + send( + sender!!, + "&aPASSED &fFake overdamage " + expectedDamage + " < " + (e.entity as LivingEntity).lastDamage + ) + tally!!.passed() + } else { + val weaponMessage = "E: " + expectedWeapon.type.name + " A: " + weaponType.name + assertEquals( + expectedDamage, e.finalDamage.toFloat(), + tally!!, weaponMessage, sender!! + ) + } + } + } + + Bukkit.getServer().pluginManager.registerEvents(listener, ocm) + + val testCount = testQueue.size.toLong() + + // Cumulative attack delay for scheduling all the tests + var attackDelay: Long = 0 + + for (test in testQueue) { + attackDelay += test.attackDelay + + Bukkit.getScheduler().runTaskLater(ocm, Runnable { + beforeEach() + test.preparations.run() + preparePlayer(test.weapon) + attacker!!.attack(defender!!) + afterEach() + }, attackDelay) + } + + Bukkit.getScheduler().runTaskLater(ocm, Runnable { + afterAll(testCount) + EntityDamageByEntityEvent.getHandlerList().unregister(listener) + }, attackDelay + 1) + } + + private fun beforeAll() { + for (player in arrayOf(attacker, defender)) { + player.gameMode = GameMode.SURVIVAL + player.maximumNoDamageTicks = 20 + player.noDamageTicks = 0 // remove spawn invulnerability + player.isInvulnerable = false + } + } + + private fun afterAll(testCount: Long) { + fakeAttacker!!.removePlayer() + fakeDefender!!.removePlayer() + + val missed = testCount - tally!!.total + sendNoPrefix( + sender!!, + "Passed: &a%d &rFailed: &c%d &rTotal: &7%d &rMissed: &7%d", + tally!!.passed, + tally!!.failed, + tally!!.total, + missed + ) + } + + private fun beforeEach() { + for (player in arrayOf(attacker, defender)) { + player.inventory.clear() + player.exhaustion = 0f + player.health = 20.0 + } + } + + private fun preparePlayer(weapon: ItemStack) { + if (weapon.hasItemMeta()) { + val meta = weapon.itemMeta + meta!!.addAttributeModifier( + Attribute.GENERIC_ATTACK_SPEED, + AttributeModifier( + UUID.randomUUID(), "speed", 1000.0, + AttributeModifier.Operation.ADD_NUMBER, EquipmentSlot.HAND + ) + ) + weapon.setItemMeta(meta) + } + attacker!!.inventory.setItemInMainHand(weapon) + attacker!!.updateInventory() + + // Update attack attribute cause it won't get done with fake players + val ai = attacker!!.getAttribute(Attribute.GENERIC_ATTACK_DAMAGE) + val defenderArmour = defender!!.getAttribute(Attribute.GENERIC_ARMOR) + + weapon.type.getDefaultAttributeModifiers(EquipmentSlot.HAND)[Attribute.GENERIC_ATTACK_DAMAGE].forEach( + Consumer { am: AttributeModifier? -> + ai!!.removeModifier(am!!) + ai.addModifier(am) + }) + + // Update armour attribute + val armourContents = defender!!.inventory.armorContents + debug("Armour: " + Arrays.stream(armourContents).filter { obj: ItemStack? -> Objects.nonNull(obj) } + .map { `is`: ItemStack -> `is`.type.name }.reduce { a: String, b: String -> "$a, $b" }.orElse("none")) + for (i in armourContents.indices) { + val itemStack = armourContents[i] ?: continue + val type = itemStack.type + val slot = + arrayOf( + EquipmentSlot.FEET, + EquipmentSlot.LEGS, + EquipmentSlot.CHEST, + EquipmentSlot.HEAD + )[i] + for (attributeModifier in type.getDefaultAttributeModifiers(slot)[Attribute.GENERIC_ARMOR]) { + defenderArmour!!.removeModifier(attributeModifier) + defenderArmour.addModifier(attributeModifier) + } + } + } + + private fun afterEach() { + for (player in arrayOf(attacker, defender)) { + player.exhaustion = 0f + player.health = 20.0 + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/OCMTest.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/OCMTest.kt new file mode 100644 index 00000000..f1781804 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/OCMTest.kt @@ -0,0 +1,16 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.tester + +import org.bukkit.inventory.ItemStack + +class OCMTest( + @JvmField val weapon: ItemStack, + @JvmField val armour: Array, + @JvmField val attackDelay: Long, + val message: String, + @JvmField val preparations: Runnable +) diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/Tally.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/Tally.kt new file mode 100644 index 00000000..58797c57 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/Tally.kt @@ -0,0 +1,24 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.tester + +class Tally { + var passed: Int = 0 + private set + var failed: Int = 0 + private set + + fun passed() { + passed++ + } + + fun failed() { + failed++ + } + + val total: Int + get() = passed + failed +} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/TesterUtils.java b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/TesterUtils.kt similarity index 56% rename from src/main/java/kernitus/plugin/OldCombatMechanics/tester/TesterUtils.java rename to src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/TesterUtils.kt index f0a2c16e..408524e2 100644 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/tester/TesterUtils.java +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/tester/TesterUtils.kt @@ -3,13 +3,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package kernitus.plugin.OldCombatMechanics.tester; +package kernitus.plugin.OldCombatMechanics.tester -import kernitus.plugin.OldCombatMechanics.utilities.Messenger; -import org.bukkit.command.CommandSender; - -public class TesterUtils { +import kernitus.plugin.OldCombatMechanics.utilities.Messenger.send +import org.bukkit.command.CommandSender +object TesterUtils { /** * Checks whether the two values are equal, prints the result and updates the tally * @@ -19,17 +18,22 @@ public class TesterUtils { * @param testName The name of the test being run * @param senders The command senders to message with the result of the test */ - public static void assertEquals(float a, float b, Tally tally, String testName, CommandSender... senders) { + @JvmStatic + fun assertEquals(a: Float, b: Float, tally: Tally, testName: String, vararg senders: CommandSender) { // Due to cooldown effects, numbers can be very close (e.g. 1.0000000149011612 == 1.0) // These are equivalent when using floats, which is what the server is using anyway if (a == b) { - tally.passed(); - for (CommandSender sender : senders) - Messenger.send(sender, "&aPASSED &f" + testName + " [E: " + a + " / A: " + b + "]"); + tally.passed() + for (sender in senders) send( + sender, + "&aPASSED &f$testName [E: $a / A: $b]" + ) } else { - tally.failed(); - for (CommandSender sender : senders) - Messenger.send(sender, "&cFAILED &f" + testName + " [E: " + a + " / A: " + b + "]"); + tally.failed() + for (sender in senders) send( + sender, + "&cFAILED &f$testName [E: $a / A: $b]" + ) } } } diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/ModuleUpdateChecker.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/ModuleUpdateChecker.kt index 502421e1..7da06a82 100644 --- a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/ModuleUpdateChecker.kt +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/ModuleUpdateChecker.kt @@ -12,7 +12,7 @@ import org.bukkit.Bukkit import org.bukkit.event.EventHandler import org.bukkit.event.player.PlayerJoinEvent -class ModuleUpdateChecker(plugin: OCMMain?) : OCMModule(plugin, "update-checker") { +class ModuleUpdateChecker(plugin: OCMMain) : OCMModule(plugin, "update-checker") { @EventHandler fun onPlayerLogin(e: PlayerJoinEvent) { val player = e.player diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/VersionChecker.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/VersionChecker.kt index 67b24728..6d421fd9 100644 --- a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/VersionChecker.kt +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/updater/VersionChecker.kt @@ -10,7 +10,7 @@ import java.util.regex.Pattern object VersionChecker { fun shouldUpdate(remoteVersion: String): Boolean { - return isUpdateOut(remoteVersion, OCMMain.getVersion()) + return isUpdateOut(remoteVersion, OCMMain.version) } private fun isUpdateOut(remoteVersion: String, localVersion: String): Boolean { diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/Config.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/Config.kt new file mode 100644 index 00000000..705c3135 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/Config.kt @@ -0,0 +1,221 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities + +import kernitus.plugin.OldCombatMechanics.ModuleLoader +import kernitus.plugin.OldCombatMechanics.ModuleLoader.toggleModules +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.module.OCMModule +import kernitus.plugin.OldCombatMechanics.utilities.damage.EntityDamageByEntityListener.Companion.INSTANCE +import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages +import org.bukkit.Bukkit +import org.bukkit.World +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.configuration.file.FileConfiguration +import org.bukkit.configuration.file.YamlConfiguration +import java.io.InputStreamReader +import java.util.* +import java.util.function.Consumer +import java.util.logging.Level + +object Config { + private const val CONFIG_NAME = "config.yml" + private lateinit var plugin: OCMMain + private lateinit var config: FileConfiguration + private val modesets: MutableMap> = HashMap() + val worlds: MutableMap> = HashMap() + + fun initialise(plugin: OCMMain) { + Config.plugin = plugin + config = plugin.config + // Make sure to separately call reload() + } + + /** + * @return Whether config was changed or not + */ + private fun checkConfigVersion(): Boolean { + val defaultConfig = YamlConfiguration.loadConfiguration( + InputStreamReader(plugin.getResource(CONFIG_NAME)!!) + ) + + if (config.getInt("config-version") != defaultConfig.getInt("config-version")) { + plugin.upgradeConfig() + reload() + return true + } + + return false + } + + + fun reload() { + if (plugin.doesConfigExist()) { + plugin.reloadConfig() + config = plugin.config + } else plugin.upgradeConfig() + + // checkConfigVersion will call #reload() again anyways + if (checkConfigVersion()) return + + Messenger.reloadConfig( + config.getBoolean("debug.enabled"), config.getString("message-prefix")!! + ) + + WeaponDamages.initialise(plugin) //Reload weapon damages from config + + //Set EntityDamagedByEntityListener to enabled if either of these modules is enabled + INSTANCE.enabled = + (moduleEnabled("old-tool-damage") || moduleEnabled("old-potion-effects") || moduleEnabled("old-critical-hits")) + + reloadModesets() + reloadWorlds() + + // Dynamically registers / unregisters all event listeners for optimal performance + toggleModules() + + ModuleLoader.modules.forEach(Consumer { module: OCMModule -> + try { + module.reload() + } catch (e: Exception) { + plugin.logger.log(Level.WARNING, "Error reloading module '$module'", e) + } + }) + } + + private fun reloadModesets() { + modesets.clear() + + val moduleNames = ModuleLoader.modules.map { it.configName }.toSet() + val modesetsSection = config.getConfigurationSection("modesets") + + // A set to keep track of all the modules that are already in a modeset + val modulesInModesets: MutableSet = HashSet() + + // Iterate over each modeset + for (key in modesetsSection!!.getKeys(false)) { + // Retrieve the list of module names for the current modeset + val moduleList = modesetsSection.getStringList(key) + val moduleSet: MutableSet = HashSet(moduleList) + + // Add the current modeset and its modules to the map + modesets[key] = moduleSet + + // Add all modules in the current modeset to the tracking set + modulesInModesets.addAll(moduleSet) + } + + // Find modules not present in any modeset + val modulesNotInAnyModeset: MutableSet = HashSet(moduleNames) + modulesNotInAnyModeset.removeAll(modulesInModesets) + + // Add any module not present in any modeset to all modesets + modesets.values.forEach { it.addAll(modulesNotInAnyModeset) } + } + + private fun reloadWorlds() { + worlds.clear() + + val worldsSection = config.getConfigurationSection("worlds") + + // Iterate over each world + for (worldName in worldsSection!!.getKeys(false)) { + val world = Bukkit.getWorld(worldName) + if (world == null) { + Messenger.warn("Configured world $worldName not found, skipping (might be loaded later?)...") + continue + } + addWorld(world, worldsSection) + } + } + + fun addWorld(world: World) { + val worldsSection = config.getConfigurationSection("worlds") + addWorld(world, worldsSection!!) + } + + fun addWorld(world: World, worldsSection: ConfigurationSection) { + // Retrieve the list of modeset names for the current world + // Using a linkedhashset to remove duplicates but retain insertion order (important for default modeset) + val modesetsSet = LinkedHashSet(worldsSection.getStringList(world.name)) + + // Add the current world and its modesets to the map + worlds[world.uid] = modesetsSet + } + + fun removeWorld(world: World) { + worlds.remove(world.uid) + } + + /** + * Get the default modeset for the given world. + * + * @param worldId The UUID for the world to check the allowed modesets for + * @return The default modeset, if found, else null. + */ + fun getDefaultModeset(worldId: UUID): Set? { + if (!worlds.containsKey(worldId)) return null + + val set = worlds[worldId] + if (set.isNullOrEmpty()) return null + + val iterator = set.iterator() + if (iterator.hasNext()) { + val modesetName = iterator.next() + if (modesets.containsKey(modesetName)) { + return modesets[modesetName] + } + } + + return null + } + + /** + * Checks whether the module is present in the default modeset for the specified world, + * or globally, is no world is specified + * + * @param moduleName The name of the module to check + * @param world The world to get the default modeset for + * @return Whether the module is enabled for the found modeset + */ + fun moduleEnabled(moduleName: String, world: World? = null): Boolean { + val section = config.getConfigurationSection(moduleName) + + if (section == null) { + plugin.logger.warning("Tried to check module '$moduleName', but it didn't exist!") + return false + } + + if (!section.getBoolean("enabled")) return false + if (world == null) return true // Only checking if module is globally enabled + + + val defaultModeset = getDefaultModeset(world.uid) ?: return true + // If no default modeset found, the module should be enabled + + // Check if module is in default modeset + return defaultModeset.contains(moduleName) + } + + fun debugEnabled(): Boolean { + return moduleEnabled("debug", null) + } + + fun moduleSettingEnabled(moduleName: String, moduleSettingName: String): Boolean { + return config.getBoolean("$moduleName.$moduleSettingName") + } + + /** + * Only use if you can't access config through plugin instance + * + * @return config.yml instance + */ + fun getConfig(): FileConfiguration = plugin.config + + @JvmStatic + fun getModesets(): Map> = modesets + +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/ConfigUtils.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/ConfigUtils.kt new file mode 100644 index 00000000..9b117dae --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/ConfigUtils.kt @@ -0,0 +1,100 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities + +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionDurations +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionTypeCompat +import org.bukkit.Material +import org.bukkit.configuration.ConfigurationSection +import java.util.* +import java.util.function.Predicate +import java.util.stream.Collectors + +/** + * Various utilities for making it easier to work with [Configurations][org.bukkit.configuration.Configuration]. + * + * @see org.bukkit.configuration.file.YamlConfiguration + * + * @see org.bukkit.configuration.ConfigurationSection + */ +object ConfigUtils { + /** + * Safely loads all doubles from a configuration section, reading both double and integer values. + * + * @param section The section from which to load the doubles. + * @return The map of doubles. + */ + fun loadDoubleMap(section: ConfigurationSection): Map { + Objects.requireNonNull(section, "section cannot be null!") + + return section.getKeys(false).stream() + .filter(Predicate { path: String -> section.isDouble(path) }.or { path: String -> + section.isInt( + path + ) + }) + .collect( + Collectors.toMap( + { key: String -> key }, + { path: String -> section.getDouble(path) }) + ) + } + + /** + * Loads the list of [Materials][Material] with the given key from a configuration section. + * Safely ignores non-matching materials. + * + * @param section The section from which to load the material list. + * @param key The key of the material list. + * @return The loaded material list, or an empty list if there is no list at the given key. + */ + @JvmStatic + fun loadMaterialList(section: ConfigurationSection, key: String): List { + Objects.requireNonNull(section, "section cannot be null!") + Objects.requireNonNull(key, "key cannot be null!") + + if (!section.isList(key)) return ArrayList() + + return section.getStringList(key).stream() + .map { name: String? -> Material.matchMaterial(name!!) } + .filter { obj: Material? -> Objects.nonNull(obj) } + .collect(Collectors.toList()) + } + + /** + * Gets potion duration values from config for all configured potion types. + * Will create map of new potion type name to durations. + * + * @param section The section from which to load the duration values + * @return HashMap of [String] and [PotionDurations] + */ + @JvmStatic + fun loadPotionDurationsList(section: ConfigurationSection): HashMap { + Objects.requireNonNull(section, "potion durations section cannot be null!") + + val durationsHashMap = HashMap() + val durationsSection = section.getConfigurationSection("potion-durations") + + val drinkableSection = durationsSection!!.getConfigurationSection("drinkable") + val splashSection = durationsSection.getConfigurationSection("splash") + + for (newPotionTypeName in drinkableSection!!.getKeys(false)) { + try { + // Get durations in seconds and convert to ticks + val drinkableDuration = drinkableSection.getInt(newPotionTypeName) * 20 + val splashDuration = splashSection!!.getInt(newPotionTypeName) * 20 + val potionTypeCompat = PotionTypeCompat(newPotionTypeName) + + durationsHashMap[potionTypeCompat] = PotionDurations(drinkableDuration, splashDuration) + } catch (e: IllegalArgumentException) { + // In case the potion doesn't exist in the version running on the server + Messenger.debug("Skipping loading $newPotionTypeName potion") + } + } + + return durationsHashMap + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/EventRegistry.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/EventRegistry.kt new file mode 100644 index 00000000..39a270b6 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/EventRegistry.kt @@ -0,0 +1,45 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities + +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.plugin.Plugin + +/** + * A simple utility class to ensure that a Listener is not registered more than once. + */ +class EventRegistry(private val plugin: Plugin) { + private val listeners: MutableList = ArrayList() + + /** + * Registers a listener and returns `true` if the listener was not already registered. + * + * @param listener The [Listener] to register. + * @return Whether the listener was successfully registered. + */ + fun registerListener(listener: Listener): Boolean { + if (listeners.contains(listener)) return false + + listeners.add(listener) + plugin.server.pluginManager.registerEvents(listener, plugin) + return true + } + + /** + * Unregisters a listener and returns `true` if the listener was already registered. + * + * @param listener The [Listener] to register. + * @return Whether the listener was successfully unregistered. + */ + fun unregisterListener(listener: Listener): Boolean { + if (!listeners.contains(listener)) return false + + listeners.remove(listener) + HandlerList.unregisterAll(listener) + return true + } +} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/MathsHelper.java b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/MathsHelper.kt similarity index 70% rename from src/main/java/kernitus/plugin/OldCombatMechanics/utilities/MathsHelper.java rename to src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/MathsHelper.kt index 5b66c0c3..18c4c39f 100644 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/MathsHelper.java +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/MathsHelper.kt @@ -3,13 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package kernitus.plugin.OldCombatMechanics.utilities; +package kernitus.plugin.OldCombatMechanics.utilities + +import kotlin.math.max +import kotlin.math.min /** * For all the maths utilities that I needed which (for some reason) aren't in the Math class. */ -public class MathsHelper { - +object MathsHelper { /** * Clamps a value between a minimum and a maximum. * @@ -18,8 +20,8 @@ public class MathsHelper { * @param max The maximum value to clamp to. * @return The clamped value. */ - public static double clamp(double value, double min, double max) { - return Math.max(Math.min(value, max), min); + @JvmStatic + fun clamp(value: Double, min: Double, max: Double): Double { + return max(min(value, max), min) } - } diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/Messenger.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/Messenger.kt new file mode 100644 index 00000000..2b38f7b0 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/Messenger.kt @@ -0,0 +1,101 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities + +import kernitus.plugin.OldCombatMechanics.OCMMain +import org.bukkit.ChatColor +import org.bukkit.command.CommandSender +import java.util.* +import java.util.logging.Level + +object Messenger { + val HORIZONTAL_BAR: String = + ChatColor.STRIKETHROUGH.toString() + "----------------------------------------------------" + private lateinit var plugin: OCMMain + + private var DEBUG_ENABLED = false + private var PREFIX = "&6[OCM]&r" + + fun initialise(plugin: OCMMain) { + Messenger.plugin = plugin + } + + fun reloadConfig(debugEnabled: Boolean, prefix: String) { + DEBUG_ENABLED = debugEnabled + PREFIX = prefix + } + + fun info(message: String, vararg args: Any?) { + plugin.logger.info(TextUtils.stripColour(String.format(message, *args))) + } + + @JvmStatic + fun warn(e: Throwable?, message: String, vararg args: Any?) { + plugin.logger.log(Level.WARNING, TextUtils.stripColour(String.format(message, *args)), e) + } + + fun warn(message: String, vararg args: Any?) { + plugin.logger.log(Level.WARNING, TextUtils.stripColour(String.format(message, *args))) + } + + /** + * This will format any ampersand (&) color codes, + * format any args passed to it using [String.format], + * and then send the message to the specified [CommandSender]. + * + * @param sender The [CommandSender] to send the message to. + * @param message The message to send. + * @param args The args to format the message with. + */ + @JvmStatic + fun sendNoPrefix(sender: CommandSender, message: String, vararg args: Any?) { + Objects.requireNonNull(sender, "sender cannot be null!") + Objects.requireNonNull(message, "message cannot be null!") + // Prevents sending of individual empty messages, allowing for selective message disabling. + if (message.isEmpty()) return + sender.sendMessage(TextUtils.colourise(String.format(message, *args))) + } + + /** + * This will add the prefix to the message, format any ampersand (&) color codes, + * format any args passed to it using [String.format], + * and then send the message to the specified [CommandSender]. + * + * @param sender The [CommandSender] to send the message to. + * @param message The message to send. + * @param prefix The prefix to the message + * @param args The args to format the message with. + */ + private fun sendWithPrefix(sender: CommandSender, message: String, prefix: String, vararg args: Any) { + // Prevents sending of individual empty messages, allowing for selective message disabling. + if (message.isEmpty()) return + sendNoPrefix(sender, "$prefix $message", *args) + } + + @JvmStatic + fun send(sender: CommandSender, message: String, vararg args: Any) { + sendWithPrefix(sender, message, PREFIX, *args) + } + + private fun sendDebugMessage(sender: CommandSender, message: String, vararg args: Any) { + sendWithPrefix(sender, message, "&1[Debug]&r", *args) + } + + fun debug(message: String?, throwable: Throwable?) { + if (DEBUG_ENABLED) plugin.logger.log(Level.INFO, message, throwable) + } + + @JvmStatic + fun debug(message: String, vararg args: Any?) { + if (DEBUG_ENABLED) info( + "[DEBUG] $message", *args + ) + } + + fun debug(sender: CommandSender, message: String, vararg args: Any) { + if (DEBUG_ENABLED) sendDebugMessage(sender, message, *args) + } +} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/TextUtils.java b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/TextUtils.kt similarity index 53% rename from src/main/java/kernitus/plugin/OldCombatMechanics/utilities/TextUtils.java rename to src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/TextUtils.kt index 95845603..d6ba0f05 100644 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/TextUtils.java +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/TextUtils.kt @@ -3,28 +3,28 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package kernitus.plugin.OldCombatMechanics.utilities; +package kernitus.plugin.OldCombatMechanics.utilities -import org.bukkit.ChatColor; +import org.bukkit.ChatColor -public class TextUtils { +object TextUtils { /** - * Converts ampersand (&) color codes to Minecraft ({@link ChatColor#COLOR_CHAR}) color codes. + * Converts ampersand (&) color codes to Minecraft ([ChatColor.COLOR_CHAR]) color codes. * * @param text The text to colourise. * @return The colourised text. */ - public static String colourise(String text) { - return ChatColor.translateAlternateColorCodes('&', text); + fun colourise(text: String): String { + return ChatColor.translateAlternateColorCodes('&', text) } /** - * Removes all Minecraft ({@link ChatColor#COLOR_CHAR}) colour codes from a string. + * Removes all Minecraft ([ChatColor.COLOR_CHAR]) colour codes from a string. * * @param text The text to strip colours from. * @return The stripped text. */ - public static String stripColour(String text) { - return ChatColor.stripColor(text); + fun stripColour(text: String?): String? { + return ChatColor.stripColor(text) } } diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/AttackCooldownTracker.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/AttackCooldownTracker.kt new file mode 100644 index 00000000..2a317780 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/AttackCooldownTracker.kt @@ -0,0 +1,52 @@ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.module.OCMModule +import kernitus.plugin.OldCombatMechanics.utilities.reflection.VersionCompatUtils +import org.bukkit.Bukkit +import org.bukkit.World +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.player.PlayerQuitEvent +import java.util.* + +/** + * Spigot versions below 1.16 did not have way of getting attack cooldown. + * Obtaining through NMS works, but value is reset before EntityDamageEvent is called. + * This means we must keep track of the cooldown to get the correct values. + */ +class AttackCooldownTracker(plugin: OCMMain) : OCMModule(plugin, "attack-cooldown-tracker") { + private val lastCooldown: MutableMap + + init { + INSTANCE = this + lastCooldown = WeakHashMap() + + val cooldownTask = Runnable { + Bukkit.getOnlinePlayers().forEach { player: Player? -> + lastCooldown[player!!.uniqueId] = VersionCompatUtils.getAttackCooldown(player) + } + } + Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, cooldownTask, 0, 1L) + } + + @EventHandler + fun onPlayerQuit(event: PlayerQuitEvent) { + lastCooldown.remove(event.player.uniqueId) + } + + // Module is always enabled, because it will only be in list of modules if server + // itself requires it (i.e. is below 1.16 / does not have getAttackCooldown method) + override fun isEnabled(world: World): Boolean { + return true + } + + companion object { + private lateinit var INSTANCE: AttackCooldownTracker + + @JvmStatic + fun getLastCooldown(uuid: UUID): Float? { + return INSTANCE.lastCooldown[uuid] + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/DamageUtils.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/DamageUtils.kt new file mode 100644 index 00000000..07ab3d63 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/DamageUtils.kt @@ -0,0 +1,100 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import kernitus.plugin.OldCombatMechanics.utilities.damage.AttackCooldownTracker.Companion.getLastCooldown +import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser +import org.bukkit.Material +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType + +object DamageUtils { + // Method added in 1.16.4 + private val isInWater: SpigotFunctionChooser = SpigotFunctionChooser.apiCompatCall( + { le: LivingEntity, params: Any? -> le.isInWater }, + { le: LivingEntity, params: Any? -> le.location.block.type == Material.WATER } + ) + + // Method added in 1.17.0 + private val isClimbing: SpigotFunctionChooser = SpigotFunctionChooser.apiCompatCall( + { le: LivingEntity, params: Any? -> le.isClimbing }, + { le: LivingEntity, params: Any? -> + val material = le.location.block.type + material == Material.LADDER || material == Material.VINE + } + ) + + // Method added in 1.16. Default parameter for reflected method is 0.5F + @JvmField + val getAttackCooldown: SpigotFunctionChooser = SpigotFunctionChooser.apiCompatCall( + { he: HumanEntity, params: Any? -> he.attackCooldown }, + { he: HumanEntity, params: Any? -> getAttackCooldown(he) } + ) + + /** + * Gets last stored attack cooldown. Used when method is not available and we are keeping track of value ourselves. + * @param humanEntity The player to get the attack cooldown from + * @return The attack cooldown, as a value between 0 and 1 + */ + private fun getAttackCooldown(humanEntity: HumanEntity): Float { + val cooldown = getLastCooldown(humanEntity.uniqueId) + if (cooldown == null) { + Messenger.debug("Last attack cooldown null for " + humanEntity.name + ", assuming full attack strength") + return 1f + } + return cooldown + } + + /** + * Get sharpness damage multiplier for 1.9 + * + * @param level The level of the enchantment + * @return Sharpness damage multiplier + */ + @JvmStatic + fun getNewSharpnessDamage(level: Int): Double { + return if (level >= 1) 1 + (level - 1) * 0.5 else 0.0 + } + + /** + * Get sharpness damage multiplier for 1.8 + * + * @param level The level of the enchantment + * @return Sharpness damage multiplier + */ + @JvmStatic + fun getOldSharpnessDamage(level: Int): Double { + return if (level >= 1) level * 1.25 else 0.0 + } + + /** + * Check preconditions for critical hit + * + * @param humanEntity Living entity to perform checks on + * @return Whether hit is critical + */ + @JvmStatic + fun isCriticalHit1_8(humanEntity: HumanEntity): Boolean { + /* Code from Bukkit 1.8_R3: + boolean flag = this.fallDistance > 0.0F && !this.onGround && !this.onClimbable() && !this.isInWater() + && !this.hasEffect(MobEffectList.BLINDNESS) && this.vehicle == null && entity instanceof EntityLiving; + */ + return humanEntity.fallDistance > 0.0f && !humanEntity.isOnGround && !isClimbing.apply(humanEntity) && !isInWater.apply( + humanEntity + ) && + humanEntity.activePotionEffects.stream().map { obj: PotionEffect -> obj.type } + .noneMatch { e: PotionEffectType -> e === PotionEffectType.BLINDNESS } && !humanEntity.isInsideVehicle + } + + @JvmStatic + fun isCriticalHit1_9(player: Player): Boolean { + return isCriticalHit1_8(player) && getAttackCooldown.apply(player) > 0.9f && !player.isSprinting + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/DefenceUtils.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/DefenceUtils.kt new file mode 100644 index 00000000..281b1512 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/DefenceUtils.kt @@ -0,0 +1,352 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector +import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser +import kernitus.plugin.OldCombatMechanics.utilities.reflection.VersionCompatUtils +import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat +import org.bukkit.Material +import org.bukkit.attribute.Attribute +import org.bukkit.attribute.AttributeModifier +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.LivingEntity +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import org.bukkit.event.entity.EntityDamageEvent.DamageModifier +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import java.util.* +import java.util.concurrent.ThreadLocalRandom +import java.util.function.Supplier +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.min + +/** + * Utilities for calculating damage reduction from armour and status effects. + * Defence order is armour defence -> resistance -> armour enchants -> absorption + * BASE -> HARD_HAT -> BLOCKING -> ARMOUR -> RESISTANCE -> MAGIC -> ABSORPTION + * This class just deals with everything from armour onwards + */ +object DefenceUtils { + private const val REDUCTION_PER_ARMOUR_POINT = 0.04 + private const val REDUCTION_PER_RESISTANCE_LEVEL = 0.2 + + private val ARMOUR_IGNORING_CAUSES: MutableSet = EnumSet.of( + DamageCause.FIRE_TICK, + DamageCause.SUFFOCATION, + DamageCause.DROWNING, + DamageCause.STARVATION, + DamageCause.FALL, + DamageCause.VOID, + DamageCause.CUSTOM, + DamageCause.MAGIC, + DamageCause.WITHER, // From 1.9 + DamageCause.FLY_INTO_WALL, + DamageCause.DRAGON_BREATH // In 1.19 FIRE bypasses armour, but it doesn't in 1.8 so we don't add it here + ) + + // Stalagmite ignores armour but other blocks under CONTACT do not, explicitly checked below + init { + if (Reflector.versionIsNewerOrEqualTo(1, 11, 0)) ARMOUR_IGNORING_CAUSES.add( + DamageCause.CRAMMING + ) + if (Reflector.versionIsNewerOrEqualTo(1, 17, 0)) ARMOUR_IGNORING_CAUSES.add( + DamageCause.FREEZE + ) + } + + // Method added in 1.15 + private val getAbsorptionAmount: SpigotFunctionChooser = + SpigotFunctionChooser.apiCompatCall( + { le: LivingEntity, _: Any? -> le.absorptionAmount }, + { le: LivingEntity, _: Any? -> + VersionCompatUtils.getAbsorptionAmount(le) + .toDouble() + }) + + /** + * Calculates the reduction by armour, resistance, armour enchantments and absorption. + * The values are updated directly in the map for each damage modifier. + * + * @param damagedEntity The entity that was damaged + * @param damageModifiers A map of the damage modifiers and their values from the event + * @param damageCause The cause of the damage + */ + @JvmStatic + @Suppress("deprecation") + fun calculateDefenceDamageReduction( + damagedEntity: LivingEntity, + damageModifiers: MutableMap, + damageCause: DamageCause, + randomness: Boolean + ) { + val armourPoints = damagedEntity.getAttribute(Attribute.GENERIC_ARMOR)!!.value + // Make sure we don't go over 100% protection + val armourReductionFactor = min(1.0, armourPoints * REDUCTION_PER_ARMOUR_POINT) + + // applyArmorModifier() calculations from NMS + // Apply armour damage reduction after hard hat (wearing helmet & hit by block) and blocking reduction + var currentDamage = damageModifiers[DamageModifier.BASE]!! + + damageModifiers.getOrDefault(DamageModifier.HARD_HAT, 0.0) + + damageModifiers.getOrDefault(DamageModifier.BLOCKING, 0.0) + if (damageModifiers.containsKey(DamageModifier.ARMOR)) { + var armourReduction = 0.0 + // If the damage cause does not ignore armour + // If the block they are in is a stalagmite, also ignore armour + if (!ARMOUR_IGNORING_CAUSES.contains(damageCause) && + !(Reflector.versionIsNewerOrEqualTo( + 1, + 19, + 0 + ) && damageCause == DamageCause.CONTACT && damagedEntity.location.block.type == Material.POINTED_DRIPSTONE) + ) { + armourReduction = currentDamage * -armourReductionFactor + } + damageModifiers[DamageModifier.ARMOR] = armourReduction + currentDamage += armourReduction + } + + // This is the applyMagicModifier() calculations from NMS + if (damageCause != DamageCause.STARVATION) { + // Apply resistance effect + if (damageModifiers.containsKey(DamageModifier.RESISTANCE) && damageCause != DamageCause.VOID && + damagedEntity.hasPotionEffect(PotionEffectTypeCompat.RESISTANCE.get()) + ) { + val level = damagedEntity.getPotionEffect(PotionEffectTypeCompat.RESISTANCE.get())!!.amplifier + 1 + // Make sure we don't go over 100% protection + val resistanceReductionFactor = min(1.0, level * REDUCTION_PER_RESISTANCE_LEVEL) + val resistanceReduction = -resistanceReductionFactor * currentDamage + damageModifiers[DamageModifier.RESISTANCE] = resistanceReduction + currentDamage += resistanceReduction + } + + // Apply armour enchants + // Don't calculate enchants if damage already 0 (like 1.8 NMS). Enchants cap at 80% reduction + if (currentDamage > 0 && damageModifiers.containsKey(DamageModifier.MAGIC)) { + val enchantsReductionFactor = calculateArmourEnchantmentReductionFactor( + damagedEntity.equipment!!.armorContents, damageCause, randomness + ) + val enchantsReduction = currentDamage * -enchantsReductionFactor + damageModifiers[DamageModifier.MAGIC] = enchantsReduction + currentDamage += enchantsReduction + } + + // Absorption + if (damageModifiers.containsKey(DamageModifier.ABSORPTION)) { + val absorptionAmount = getAbsorptionAmount.apply(damagedEntity) + val absorptionReduction = -min(absorptionAmount, currentDamage) + damageModifiers[DamageModifier.ABSORPTION] = absorptionReduction + } + } + } + + /** + * Return the damage after applying armour, resistance, and armour enchants protections, following 1.8 algorithm. + * + * @param defender The entity that is being attacked + * @param baseDamage The base damage done by the event, including weapon enchants, potions, crits + * @param armourContents The 4 pieces of armour contained in the armour slots + * @param damageCause The source of damage + * @param randomness Whether to apply random multiplier + * @return The damage done to the entity after armour is taken into account + */ + fun getDamageAfterArmour1_8( + defender: LivingEntity, + baseDamage: Double, + armourContents: Array, + damageCause: DamageCause, + randomness: Boolean + ): Double { + var armourPoints = 0.0 + for (i in armourContents.indices) { + val itemStack = armourContents[i] ?: continue + val slot = + arrayOf( + EquipmentSlot.FEET, + EquipmentSlot.LEGS, + EquipmentSlot.CHEST, + EquipmentSlot.HEAD + )[i] + armourPoints += getAttributeModifierSum(itemStack.type.getDefaultAttributeModifiers(slot)[Attribute.GENERIC_ARMOR]) + } + + val reductionFactor = armourPoints * REDUCTION_PER_ARMOUR_POINT + + // Apply armour damage reduction + var finalDamage = + baseDamage - (if (ARMOUR_IGNORING_CAUSES.contains(damageCause)) 0.0 else (baseDamage * reductionFactor)) + + // Calculate resistance + if (defender.hasPotionEffect(PotionEffectTypeCompat.RESISTANCE.get())) { + val resistanceLevel = defender.getPotionEffect(PotionEffectTypeCompat.RESISTANCE.get())!!.amplifier + 1 + finalDamage *= 1.0 - (resistanceLevel * 0.2) + } + + // Don't calculate enchantment reduction if damage is already 0. NMS 1.8 does it this way. + val enchantmentReductionFactor = + calculateArmourEnchantmentReductionFactor(armourContents, damageCause, randomness) + if (finalDamage > 0) { + finalDamage -= finalDamage * enchantmentReductionFactor + } + + Messenger.debug( + "Reductions: Armour %.0f%%, Ench %.0f%%, Total %.2f%%, Start dmg: %.2f Final: %.2f", + reductionFactor * 100, + enchantmentReductionFactor * 100, + (reductionFactor + (1 - reductionFactor) * enchantmentReductionFactor) * 100, + baseDamage, + finalDamage + ) + + return finalDamage + } + + /** + * Applies all the operations for the attribute modifiers of a specific attribute. + * Does not take into account the base value. + */ + private fun getAttributeModifierSum(modifiers: Collection): Double { + var sum = 0.0 + for (modifier in modifiers) { + val value = modifier.amount + when (modifier.operation) { + AttributeModifier.Operation.ADD_SCALAR -> sum += abs(value) + AttributeModifier.Operation.ADD_NUMBER -> sum += value + AttributeModifier.Operation.MULTIPLY_SCALAR_1 -> sum *= value + } + } + return sum + } + + + private fun calculateArmourEnchantmentReductionFactor( + armourContents: Array, + cause: DamageCause, + randomness: Boolean + ): Double { + var totalEpf = 0 + for (armourItem in armourContents) { + if (armourItem != null && armourItem.type != Material.AIR) { + for (enchantmentType in EnchantmentType.entries) { + if (!enchantmentType.protectsAgainst(cause)) continue + + val enchantmentLevel = armourItem.getEnchantmentLevel(enchantmentType.enchantment) + + if (enchantmentLevel > 0) { + totalEpf += enchantmentType.getEpf(enchantmentLevel) + } + } + } + } + + // Cap at 25 + totalEpf = min(25.0, totalEpf.toDouble()).toInt() + + // Multiply by random value between 50% and 100%, then round up + val multiplier = if (randomness) ThreadLocalRandom.current().nextDouble(0.5, 1.0) else 1.0 + totalEpf = ceil(totalEpf * multiplier).toInt() + + // Cap at 20 + totalEpf = min(20.0, totalEpf.toDouble()).toInt() + + return REDUCTION_PER_ARMOUR_POINT * totalEpf + } + + private enum class EnchantmentType( + protection: Supplier>, private val typeModifier: Double, + /** + * Returns the bukkit enchantment. + * + * @return the bukkit enchantment + */ + val enchantment: Enchantment + ) { + // Data from https://minecraft.fandom.com/wiki/Armor#Mechanics + PROTECTION( + Supplier { + val damageCauses = EnumSet.of( + DamageCause.CONTACT, + DamageCause.ENTITY_ATTACK, + DamageCause.PROJECTILE, + DamageCause.FALL, + DamageCause.FIRE, + DamageCause.LAVA, + DamageCause.BLOCK_EXPLOSION, + DamageCause.ENTITY_EXPLOSION, + DamageCause.LIGHTNING, + DamageCause.POISON, + DamageCause.MAGIC, + DamageCause.WITHER, + DamageCause.FALLING_BLOCK, + DamageCause.THORNS, + DamageCause.DRAGON_BREATH + ) + if (Reflector.versionIsNewerOrEqualTo(1, 10, 0)) damageCauses.add( + DamageCause.HOT_FLOOR + ) + if (Reflector.versionIsNewerOrEqualTo(1, 12, 0)) damageCauses.add( + DamageCause.ENTITY_SWEEP_ATTACK + ) + damageCauses + }, + 0.75, EnchantmentCompat.PROTECTION.get()!! + ), + FIRE_PROTECTION(Supplier { + val damageCauses = EnumSet.of( + DamageCause.FIRE, + DamageCause.FIRE_TICK, + DamageCause.LAVA + ) + if (Reflector.versionIsNewerOrEqualTo(1, 10, 0)) { + damageCauses.add(DamageCause.HOT_FLOOR) + } + damageCauses + }, 1.25, EnchantmentCompat.FIRE_PROTECTION.get()!!), + BLAST_PROTECTION(Supplier { + EnumSet.of( + DamageCause.ENTITY_EXPLOSION, + DamageCause.BLOCK_EXPLOSION + ) + }, 1.5, EnchantmentCompat.BLAST_PROTECTION.get()!!), + PROJECTILE_PROTECTION(Supplier { + EnumSet.of( + DamageCause.PROJECTILE + ) + }, 1.5, EnchantmentCompat.PROJECTILE_PROTECTION.get()!!), + FALL_PROTECTION(Supplier { + EnumSet.of( + DamageCause.FALL + ) + }, 2.5, EnchantmentCompat.FEATHER_FALLING.get()!!); + + private val protection = protection.get() + + /** + * Returns whether the armour protects against the given damage cause. + * + * @param cause the damage cause + * @return true if the armour protects against the given damage cause + */ + fun protectsAgainst(cause: DamageCause): Boolean { + return protection.contains(cause) + } + + /** + * Returns the enchantment protection factor (EPF). + * + * @param level the level of the enchantment + * @return the EPF + */ + fun getEpf(level: Int): Int { + // floor ( (6 + level^2) * TypeModifier / 3 ) + return floor((6 + level * level) * typeModifier / 3).toInt() + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/EntityDamageByEntityListener.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/EntityDamageByEntityListener.kt new file mode 100644 index 00000000..0de16c28 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/EntityDamageByEntityListener.kt @@ -0,0 +1,235 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.module.OCMModule +import org.bukkit.Bukkit +import org.bukkit.World +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.LivingEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent +import java.util.* + +class EntityDamageByEntityListener(plugin: OCMMain) : OCMModule(plugin, "entity-damage-listener") { + var enabled: Boolean = false + + private val lastDamages: MutableMap + + init { + INSTANCE = this + lastDamages = WeakHashMap() + } + + override fun isEnabled() = enabled + override fun isEnabled(world: World) = enabled + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + fun onEntityDamage(event: EntityDamageEvent) { + val damagee = event.entity + + if (event !is EntityDamageByEntityEvent) { + // Damage immunity only applies to living entities + if (damagee !is LivingEntity) return + val livingDamagee = damagee + + restoreLastDamage(livingDamagee) + + var newDamage = event.damage // base damage, before defence calculations + + // Overdamage due to immunity + // Invulnerability will cause less damage if they attack with a stronger weapon while vulnerable + // That is, the difference in damage will be dealt, but only if new attack is stronger than previous one + checkOverdamage(livingDamagee, event, newDamage) + + if (newDamage < 0) { + debug("Damage was $newDamage setting to 0") + newDamage = 0.0 + } + + // Set damage, this should scale effects in the 1.9 way in case some of our modules are disabled + event.damage = newDamage + debug("Attack damage (before defence): $newDamage") + } else { + val damager = event.damager + + // Call event constructor before setting lastDamage back, because we need it for calculations + val e = OCMEntityDamageByEntityEvent(damager, damagee, event.getCause(), event.getDamage()) + + // Set last damage to actual value for other modules and plugins to use + // This will be set back to 0 in MONITOR listener on the next tick to detect all potential overdamages. + // If there is large delay between last time an entity was damaged and the next damage, + // the last damage might have been removed from the weak hash map. This is intended, as the immunity + // ticks tends to be a short period of time anyway and last damage is irrelevant after immunity has expired. + if (damagee is LivingEntity) restoreLastDamage(damagee) + + // Call event for the other modules to make their modifications + plugin.server.pluginManager.callEvent(e) + + if (e.isCancelled) return + + // Now we re-calculate damage modified by the modules and set it back to original event + // Attack components order: (Base + Potion effects, scaled by attack delay) + Critical Hit + (Enchantments, scaled by attack delay) + // Hurt components order: Overdamage - Armour - Resistance - Armour enchants - Absorption + var newDamage = e.baseDamage + + debug("Base: " + e.baseDamage, damager) + debug("Base: " + e.baseDamage) + + // Weakness potion + val weaknessModifier = e.weaknessModifier * e.weaknessLevel + val weaknessAddend = if (e.isWeaknessModifierMultiplier) newDamage * weaknessModifier else weaknessModifier + // Don't modify newDamage yet so both potion effects are calculated off of the base damage + debug("Weak: $weaknessAddend") + debug("Weak: $weaknessAddend", damager) + + // Strength potion + debug("Strength level: " + e.strengthLevel) + debug("Strength level: " + e.strengthLevel, damager) + var strengthModifier = e.strengthModifier * e.strengthLevel + if (!e.isStrengthModifierMultiplier) newDamage += strengthModifier + else if (e.isStrengthModifierAddend) newDamage *= ++strengthModifier + else newDamage *= strengthModifier + + debug("Strength: $strengthModifier") + debug("Strength: $strengthModifier", damager) + + newDamage += weaknessAddend + + // Scale by attack delay + // float currentItemAttackStrengthDelay = 1.0D / GenericAttributes.ATTACK_SPEED * 20.0D + // attack strength ticker goes up by 1 every tick, is reset to 0 after an attack + // float f2 = MathHelper.clamp((attackStrengthTicker + 0.5) / currentItemAttackStrengthDelay, 0.0F, 1.0F); + // f *= 0.2F + f2 * f2 * 0.8F; + // the multiplier is equivalent to y = 0.8x^2 + 0.2 + // because x (f2) is always between 0 and 1, the multiplier will always be between 0.2 and 1 + // this implies 40 speed is the minimum to always have full attack strength + if (damager is HumanEntity) { + val cooldown = DamageUtils.getAttackCooldown.apply(damager, 0.5f) // i.e. f2 + debug("Scale by attack delay: $newDamage *= 0.2 + $cooldown^2 * 0.8") + newDamage *= (0.2f + cooldown * cooldown * 0.8f).toDouble() + } + + // Critical hit + val criticalMultiplier = e.criticalMultiplier + debug("Crit $newDamage *= $criticalMultiplier") + newDamage *= criticalMultiplier + + // Enchantment damage, scaled by attack cooldown + var enchantmentDamage = e.mobEnchantmentsDamage + e.sharpnessDamage + if (damager is HumanEntity) { + val cooldown = DamageUtils.getAttackCooldown.apply(damager, 0.5f) + debug("Scale enchantments by attack delay: $enchantmentDamage *= $cooldown") + enchantmentDamage *= cooldown.toDouble() + } + newDamage += enchantmentDamage + debug( + "Mob " + e.mobEnchantmentsDamage + " Sharp: " + e.sharpnessDamage + " Scaled: " + enchantmentDamage, + damager + ) + + if (damagee is LivingEntity) { + // Overdamage due to immunity + // Invulnerability will cause less damage if they attack with a stronger weapon while vulnerable + // That is, the difference in damage will be dealt, but only if new attack is stronger than previous one + // Value before overdamage will become new "last damage" + newDamage = checkOverdamage(damagee, event, newDamage) + } + + if (newDamage < 0) { + debug("Damage was $newDamage setting to 0", damager) + newDamage = 0.0 + } + + // Set damage, this should scale effects in the 1.9 way in case some of our modules are disabled + event.setDamage(newDamage) + debug("New Damage: $newDamage", damager) + debug("Attack damage (before defence): $newDamage") + } + } + + /** + * Set entity's last damage to 0 a tick after the event so all overdamage attacks get through. + * The last damage is overridden by NMS code regardless of what the actual damage is set to via Spigot. + * Finally, the LOWEST priority listener above will set the last damage back to the correct value + * for other plugins to use the next time the entity is damaged. + */ + @EventHandler(priority = EventPriority.MONITOR) + fun afterEntityDamage(event: EntityDamageEvent) { + val damagee = event.entity + + if (event is EntityDamageByEntityEvent) { + if (lastDamages.containsKey(damagee.uniqueId)) { + // Set last damage to 0, so we can detect attacks even by weapons with a weaker attack value than what OCM would calculate + Bukkit.getScheduler().runTaskLater(plugin, Runnable { + (damagee as LivingEntity).lastDamage = 0.0 + debug("Set last damage to 0", damagee) + debug("Set last damage to 0") + }, 1L) + } + } else { + // if not EDBYE then we leave last damage as is + lastDamages.remove(damagee.uniqueId) + debug("Non-entity damage, using default last damage", damagee) + debug("Non-entity damage, using default last damage") + } + } + + /** + * Restored the correct last damage for the given entity + * + * @param damagee The living entity to try to restore the last damage for + */ + private fun restoreLastDamage(damagee: LivingEntity) { + val lastStoredDamage = lastDamages[damagee.uniqueId] + if (lastStoredDamage != null) { + val livingDamagee = damagee + livingDamagee.lastDamage = lastStoredDamage + debug("Set last damage back to $lastStoredDamage", livingDamagee) + debug("Set last damage back to $lastStoredDamage") + } + } + + private fun checkOverdamage(livingDamagee: LivingEntity, event: EntityDamageEvent, newDamage: Double): Double { + var newDamage = newDamage + val newLastDamage = newDamage + + if (livingDamagee.noDamageTicks.toFloat() > livingDamagee.maximumNoDamageTicks.toFloat() / 2.0f) { + // Last damage was either set to correct value above in this listener, or we're using the server's value + // If other plugins later modify BASE damage, they should either be taking last damage into account, + // or ignoring the event if it is cancelled + val lastDamage = livingDamagee.lastDamage + if (newDamage <= lastDamage) { + event.damage = 0.0 + event.isCancelled = true + debug("Was fake overdamage, cancelling $newDamage <= $lastDamage") + return 0.0 + } + + debug("Overdamage: $newDamage - $lastDamage") + // We must subtract previous damage from new weapon damage for this attack + newDamage -= livingDamagee.lastDamage + + debug( + ("Last damage " + lastDamage + " new damage: " + newLastDamage + " applied: " + newDamage + + " ticks: " + livingDamagee.noDamageTicks + " /" + livingDamagee.maximumNoDamageTicks) + ) + } + // Update the last damage done, including when it was overdamage. + // This means attacks must keep increasing in value during immunity period to keep dealing overdamage. + lastDamages[livingDamagee.uniqueId] = newLastDamage + + return newDamage + } + + + companion object { + lateinit var INSTANCE: EntityDamageByEntityListener + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/MobDamage.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/MobDamage.kt new file mode 100644 index 00000000..f0187ddd --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/MobDamage.kt @@ -0,0 +1,76 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import com.google.common.collect.ImmutableMap +import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.EntityType +import org.bukkit.inventory.ItemStack +import java.util.function.Consumer + +object MobDamage { + private val enchants: Map + private val SMITE = EnchantmentCompat.SMITE.get() + private val BANE_OF_ARTHROPODS = EnchantmentCompat.BANE_OF_ARTHROPODS.get() + + init { + val allMobs: Map = + ImmutableMap.builder() // Undead (https://minecraft.gamepedia.com/Undead) + .put("SKELETON", SMITE) + .put("ZOMBIE", SMITE) + .put("WITHER", SMITE) + .put("WITHER_SKELETON", SMITE) + .put("ZOMBIFIED_PIGLIN", SMITE) + .put("SKELETON_HORSE", SMITE) + .put("STRAY", SMITE) + .put("HUSK", SMITE) + .put("PHANTOM", SMITE) + .put("DROWNED", SMITE) + .put("ZOGLIN", SMITE) + .put("ZOMBIE_HORSE", SMITE) + .put("ZOMBIE_VILLAGER", SMITE) // Arthropods (https://minecraft.gamepedia.com/Arthropod) + + .put("SPIDER", BANE_OF_ARTHROPODS) + .put("CAVE_SPIDER", BANE_OF_ARTHROPODS) + .put("BEE", BANE_OF_ARTHROPODS) + .put("SILVERFISH", BANE_OF_ARTHROPODS) + .put("ENDERMITE", BANE_OF_ARTHROPODS) + + .build() + + val enchantsBuilder = ImmutableMap.builder() + + // Add these individually because some may not exist in the Minecraft version we're running + allMobs.keys.forEach(Consumer { entityName: String -> + try { + val entityType = EntityType.valueOf(entityName) + val enchantment = allMobs[entityName] + if (enchantment != null) { + enchantsBuilder.put(entityType, enchantment) + } + } catch (ignored: IllegalArgumentException) { + } // Mob not supported in this MC version + }) + enchants = enchantsBuilder.build() + } + + /** + * Gets damage due to Smite and Bane of Arthropods enchantments, when applicable + * + * @param entity The type of entity that was attacked + * @param item The enchanted weapon used in the attack + * @return The damage due to the enchantments + */ + @JvmStatic + fun getEntityEnchantmentsDamage(entity: EntityType, item: ItemStack): Double { + val enchantment = enchants[entity] + + if (enchantment == null || enchantment !== SMITE || enchantment !== BANE_OF_ARTHROPODS) return 0.0 + + return 2.5 * item.getEnchantmentLevel(enchantment) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/NewWeaponDamage.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/NewWeaponDamage.kt new file mode 100644 index 00000000..95472121 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/NewWeaponDamage.kt @@ -0,0 +1,39 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import org.bukkit.Material + +/** + * Default 1.9 Minecraft tool damage values + */ +enum class NewWeaponDamage(val damage: Float) { + // common values + STONE_SWORD(5f), STONE_SHOVEL(3.5f), STONE_PICKAXE(3f), STONE_AXE(9f), STONE_HOE(1f), + IRON_SWORD(6f), IRON_SHOVEL(4.5f), IRON_PICKAXE(4f), IRON_AXE(9f), IRON_HOE(1f), + DIAMOND_SWORD(7f), DIAMOND_SHOVEL(5.5f), DIAMOND_PICKAXE(5f), DIAMOND_AXE(9f), DIAMOND_HOE(1f), + + // pre-1.13 values + STONE_SPADE(3.5f), IRON_SPADE(4.5f), DIAMOND_SPADE(5.5f), + WOOD_SWORD(4f), WOOD_SPADE(2.5f), WOOD_PICKAXE(2f), WOOD_AXE(7f), WOOD_HOE(1f), + GOLD_SWORD(4f), GOLD_SPADE(2.5f), GOLD_PICKAXE(2f), GOLD_AXE(7f), GOLD_HOE(1f), + + // post-1.13 values + WOODEN_SWORD(4f), WOODEN_SHOVEL(2.5f), WOODEN_PICKAXE(2f), WOODEN_AXE(7f), WOODEN_HOE(1f), + GOLDEN_SWORD(4f), GOLDEN_SHOVEL(2.5f), GOLDEN_PICKAXE(2f), GOLDEN_AXE(7f), GOLDEN_HOE(1f), + NETHERITE_SWORD(8f), NETHERITE_SHOVEL(6.5f), NETHERITE_PICKAXE(6f), NETHERITE_AXE(10f), NETHERITE_HOE(1f); + + companion object { + fun getDamage(mat: String?): Float { + return valueOf(mat!!).damage + } + + @JvmStatic + fun getDamage(mat: Material): Float { + return getDamage(mat.toString()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/OCMEntityDamageByEntityEvent.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/OCMEntityDamageByEntityEvent.kt new file mode 100644 index 00000000..d1ff52f4 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/OCMEntityDamageByEntityEvent.kt @@ -0,0 +1,245 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils.getNewSharpnessDamage +import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils.isCriticalHit1_8 +import kernitus.plugin.OldCombatMechanics.utilities.damage.DamageUtils.isCriticalHit1_9 +import kernitus.plugin.OldCombatMechanics.utilities.damage.MobDamage.getEntityEnchantmentsDamage +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat +import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffects +import kernitus.plugin.OldCombatMechanics.versions.enchantments.EnchantmentCompat +import org.bukkit.Material +import org.bukkit.entity.Entity +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList +import org.bukkit.event.entity.EntityDamageEvent.DamageCause +import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffectType + +class OCMEntityDamageByEntityEvent( + @JvmField val damager: Entity, @JvmField val damagee: Entity, @JvmField val cause: DamageCause, rawDamage: Double +) : Event(), Cancellable { + private var cancelled = false + override fun getHandlers(): HandlerList { + return handlerList + } + + + var rawDamage: Double + private set + + var weapon: ItemStack? + private set + + @JvmField + val sharpnessLevel: Int + private val hasWeakness: Boolean + + // The levels as shown in-game, i.e. 1 or 2 corresponding to I and II + val strengthLevel: Int + + @JvmField + var weaknessLevel: Int + + @JvmField + var baseDamage: Double = 0.0 + var mobEnchantmentsDamage: Double = 0.0 + + @JvmField + var sharpnessDamage: Double = 0.0 + + @JvmField + var criticalMultiplier: Double = 1.0 + + @JvmField + var strengthModifier: Double = 0.0 + + @JvmField + var weaknessModifier: Double = 0.0 + + // In 1.9 strength modifier is an addend, in 1.8 it is a multiplier and addend (+130%) + @JvmField + var isStrengthModifierMultiplier: Boolean = false + + @JvmField + var isStrengthModifierAddend: Boolean = true + + @JvmField + var isWeaknessModifierMultiplier: Boolean = false + + private var was1_8Crit = false + private var wasSprinting = false + + // Here we reverse-engineer all the various damages caused by removing them one at a time, backwards from what NMS code does. + // This is so the modules can listen to this event and make their modifications, then EntityDamageByEntityListener sets the new values back. + // Performs the opposite of the following: + // (Base + Potion effects, scaled by attack delay) + Critical Hit + (Enchantments, scaled by attack delay), Overdamage, Armour + init { + // We ignore attacks like arrows etc. because we do not need to change the attack side of those + // Other modules such as old armour strength work independently of this event + if (damager !is LivingEntity) { + isCancelled = true + } + + // The raw damage passed to this event is EDBE's BASE damage, which does not include armour effects or resistance etc (defence) + this.rawDamage = rawDamage + + /* + Invulnerability will cause less damage if they attack with a stronger weapon while vulnerable. + We must detect this and account for it, instead of setting the usual base weapon damage. + We artificially set the last damage to 0 between events so that all hits will register, + however we only do this for DamageByEntity, so there could still be environmental damage (e.g. cactus). + */ + if (damagee is LivingEntity) { + val livingDamagee = damagee + if (livingDamagee.noDamageTicks.toFloat() > livingDamagee.maximumNoDamageTicks.toFloat() / 2.0f) { + // NMS code also checks if current damage is higher that previous damage. However, here the event + // already has the difference between the two as the raw damage, and the event does not fire at all + // if this precondition is not met. + + // Adjust for last damage being environmental sources (e.g. cactus, fall damage) + + val lastDamage = livingDamagee.lastDamage + this.rawDamage = rawDamage + lastDamage + + Messenger.debug( + livingDamagee, + "Overdamaged!: " + livingDamagee.noDamageTicks + "/" + livingDamagee.maximumNoDamageTicks + " last: " + livingDamagee.lastDamage + ) + } else { + Messenger.debug( + livingDamagee, + "Invulnerability: " + livingDamagee.noDamageTicks + "/" + livingDamagee.maximumNoDamageTicks + " last: " + livingDamagee.lastDamage + ) + } + } + + val livingDamager = damager as LivingEntity + + weapon = livingDamager.equipment!!.itemInMainHand + // Yay paper. Why do you need to return null here? + if (weapon == null) weapon = ItemStack(Material.AIR) + + // Technically the weapon could be in the offhand, i.e. a bow. + // However, we are only concerned with melee weapons here, which will always be in the main hand. + val damageeType = damagee.type + + Messenger.debug(livingDamager, "Raw attack damage: $rawDamage") + Messenger.debug(livingDamager, "Without overdamage: " + this.rawDamage) + + + mobEnchantmentsDamage = getEntityEnchantmentsDamage(damageeType, weapon!!) + sharpnessLevel = weapon!!.getEnchantmentLevel(EnchantmentCompat.SHARPNESS.get()) + sharpnessDamage = getNewSharpnessDamage(sharpnessLevel) + + // Scale enchantment damage by attack cooldown + if (damager is HumanEntity) { + val cooldown = DamageUtils.getAttackCooldown.apply(damager, 0.5f) + mobEnchantmentsDamage *= cooldown.toDouble() + sharpnessDamage *= cooldown.toDouble() + } + + Messenger.debug( + livingDamager, "Mob: $mobEnchantmentsDamage Sharpness: $sharpnessDamage" + ) + + // Amount of damage including potion effects and critical hits + var tempDamage = this.rawDamage - mobEnchantmentsDamage - sharpnessDamage + + Messenger.debug(livingDamager, "No ench damage: $tempDamage") + + // Check if it's a critical hit + if (livingDamager is Player && isCriticalHit1_8(livingDamager as HumanEntity)) { + was1_8Crit = true + Messenger.debug(livingDamager, "1.8 Critical hit detected") + // In 1.9 a crit also requires the player not to be sprinting + if (isCriticalHit1_9(livingDamager)) { + Messenger.debug(livingDamager, "1.9 Critical hit detected") + Messenger.debug("1.9 Critical hit detected") + criticalMultiplier = 1.5 + tempDamage /= 1.5 + } + } + + // Un-scale the damage by the attack strength + if (damager is HumanEntity) { + val cooldown = DamageUtils.getAttackCooldown.apply(damager, 0.5f) + tempDamage /= (0.2f + cooldown * cooldown * 0.8f).toDouble() + } + + // amplifier 0 = Strength I amplifier 1 = Strength II + strengthLevel = (PotionEffects.get(livingDamager, PotionEffectTypeCompat.STRENGTH.get())?.amplifier ?: -1) + 1 + + strengthModifier = (strengthLevel * 3).toDouble() + + Messenger.debug( + livingDamager, "Strength Modifier: $strengthModifier" + ) + + // Don't set has weakness if amplifier is > 0 or < -1, which is outside normal range and probably set by plugin + // We use an amplifier of -1 (Level 0) to have no effect so weaker attacks will register + val weaknessAmplifier = PotionEffects.get(livingDamager, PotionEffectType.WEAKNESS)?.amplifier + hasWeakness = weaknessAmplifier != null && (weaknessAmplifier == -1 || weaknessAmplifier == 0) + weaknessLevel = (weaknessAmplifier ?: -1) + 1 + + weaknessModifier = (weaknessLevel * -4).toDouble() + + Messenger.debug( + livingDamager, "Weakness Modifier: $weaknessModifier" + ) + + baseDamage = tempDamage + weaknessModifier - strengthModifier + Messenger.debug(livingDamager, "Base tool damage: $baseDamage") + } + + /** + * Whether the attacker had the weakness potion effect, + * and the level of the effect was either 0 (used by OCM) or 1 (normal value). + * Values outside this range are to be ignored, as they are probably from other plugins. + */ + fun hasWeakness(): Boolean { + return hasWeakness + } + + override fun isCancelled(): Boolean { + return cancelled + } + + override fun setCancelled(cancelled: Boolean) { + this.cancelled = cancelled + } + + fun wasSprinting(): Boolean { + return wasSprinting + } + + fun setWasSprinting(wasSprinting: Boolean) { + this.wasSprinting = wasSprinting + } + + fun was1_8Crit(): Boolean { + return was1_8Crit + } + + fun setWas1_8Crit(was1_8Crit: Boolean) { + this.was1_8Crit = was1_8Crit + } + + companion object { + private val handlerList: HandlerList = HandlerList() + + @JvmStatic // Make sure Java reflection can access this + fun getHandlerList(): HandlerList { + return handlerList + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/WeaponDamages.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/WeaponDamages.kt new file mode 100644 index 00000000..a664cf6a --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/damage/WeaponDamages.kt @@ -0,0 +1,47 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.damage + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.utilities.ConfigUtils +import org.bukkit.Material +import java.util.* + +object WeaponDamages { + private var damages: Map = emptyMap() + + private lateinit var plugin: OCMMain + + @JvmStatic + fun initialise(plugin: OCMMain) { + WeaponDamages.plugin = plugin + reload() + } + + private fun reload() { + val section = plugin.config.getConfigurationSection("old-tool-damage.damages") + damages = ConfigUtils.loadDoubleMap(section!!) // Should error out if damages not found + } + + @JvmStatic + fun getDamage(mat: Material): Double { + val name = mat.name.replace("GOLDEN", "GOLD").replace("WOODEN", "WOOD").replace("SHOVEL", "SPADE") + return damages.getOrDefault(name, -1.0) + } + + @JvmStatic + val materialDamages: Map + get() { + val materialMap: MutableMap = + EnumMap(org.bukkit.Material::class.java) + damages.forEach { (name: String, damage: Double) -> + val newName = + name.replace("GOLD", "GOLDEN").replace("WOOD", "WOODEN").replace("SPADE", "SHOVEL") + materialMap[Material.valueOf(newName)] = damage + } + return materialMap + } +} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionDurations.java b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionDurations.kt similarity index 65% rename from src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionDurations.java rename to src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionDurations.kt index 82ba0c36..f5c9c639 100644 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionDurations.java +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionDurations.kt @@ -3,10 +3,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package kernitus.plugin.OldCombatMechanics.utilities.potions; +package kernitus.plugin.OldCombatMechanics.utilities.potions /** * Hold information on duration of drinkable & splash version of a potion type */ -public record PotionDurations(int drinkable, int splash) { -} +@JvmRecord +data class PotionDurations(@JvmField val drinkable: Int, @JvmField val splash: Int) diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffectTypeCompat.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffectTypeCompat.kt new file mode 100644 index 00000000..9e7dd220 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffectTypeCompat.kt @@ -0,0 +1,49 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.potions + +import org.bukkit.potion.PotionEffectType + +enum class PotionEffectTypeCompat(oldName: String) { + RESISTANCE("DAMAGE_RESISTANCE"), + NAUSEA("CONFUSION"), + HASTE("FAST_DIGGING"), + INSTANT_DAMAGE("HARM"), + INSTANT_HEALTH("HEAL"), + STRENGTH("INCREASE_DAMAGE"), + JUMP_BOOST("JUMP"), + SLOWNESS("SLOW"), + MINING_FATIGUE("SLOW_DIGGING"), + ; + + private var potionEffectType: PotionEffectType = + PotionEffectType.getByName(name) + ?: PotionEffectType.getByName(oldName) + ?: throw IllegalStateException("PotionEffectType not found for: $name or $oldName") + + fun get(): PotionEffectType { + return potionEffectType + } + + companion object { + /** + * Gets correct PotionEffectType for currently-running server version given new name. + * @param newName The PotionEffectType >=1.20.6 name + * @return The PotionEffectType for the currently-running server version, or null if not found. + */ + @JvmStatic + fun fromNewName(newName: String): PotionEffectType? { + return try { + // See if new name needs mapping to old + valueOf(newName.uppercase()) + .get() + } catch (e: IllegalArgumentException) { + // Otherwise use new name directly + PotionEffectType.getByName(newName) + } + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffects.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffects.kt new file mode 100644 index 00000000..688a1bd1 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionEffects.kt @@ -0,0 +1,30 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.potions + +import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser +import org.bukkit.entity.LivingEntity +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType + +object PotionEffects { + private val getPotionEffectsFunction: SpigotFunctionChooser = + SpigotFunctionChooser.apiCompatCall({ le, type -> le.getPotionEffect(type) }, { le, type -> + le.activePotionEffects.stream().filter { potionEffect -> potionEffect.type == type }.findAny().orElse(null) + }) + + /** + * Returns the [PotionEffect] of a given [PotionEffectType] for a given [LivingEntity], if present. + * + * @param entity the entity to query + * @param type the type to search + * @return the [PotionEffect] if present + */ + fun get(entity: LivingEntity, type: PotionEffectType): PotionEffect? { + return getPotionEffectsFunction.apply(entity, type) + } + +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionTypeCompat.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionTypeCompat.kt new file mode 100644 index 00000000..edef6b00 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/potions/PotionTypeCompat.kt @@ -0,0 +1,150 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.potions + +import org.bukkit.inventory.meta.PotionMeta +import org.bukkit.potion.PotionType + +class PotionTypeCompat { + /** + * Map new potion type names to old (pre 1.20.5) + */ + private enum class PotionTypeMapper(val oldName: String) { + HARMING("INSTANT_DAMAGE"), + STRONG_HARMING("INSTANT_DAMAGE"), + HEALING("INSTANT_HEAL"), + STRONG_HEALING("INSTANT_HEAL"), + LEAPING("JUMP"), + STRONG_LEAPING("JUMP"), + LONG_LEAPING("JUMP"), + REGENERATION("REGEN"), + STRONG_REGENERATION("REGEN"), + LONG_REGENERATION("REGEN"), + SWIFTNESS("SPEED"), + STRONG_SWIFTNESS("SPEED"), + LONG_SWIFTNESS("SPEED") + } + + private val oldName: String + + @JvmField + val newName: String + val type: PotionType? + + @JvmField + val isStrong: Boolean + val isLong: Boolean + + override fun hashCode(): Int { + return newName.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is PotionTypeCompat && newName == other.newName + } + + /** + * Get PotionType for currently-running server. Requires newName and oldName to already be set. + * + * @return PotionType if found. + * @throws IllegalArgumentException If potion type could not be found. + */ + private fun getPotionType(): PotionType? { + var potionType = try { + PotionType.valueOf(this.newName) + } catch (e: IllegalArgumentException) { + // If running >=1.20.5, UNCRAFTABLE has been turned into null type + + if (this.oldName == "UNCRAFTABLE") return null + + try { + PotionType.valueOf(this.oldName) + } catch (exception: IllegalArgumentException) { + throw IllegalArgumentException("Invalid potion type, tried $newName and $oldName", exception) + } + } + return potionType + } + + /** + * Create an instance of [PotionTypeCompat] starting from the new potion name. (>=1.20.5) + * Name will be turned into upper case as needed. + * + * @param newName The new potion type name, e.g. strong_leaping. + */ + constructor(newName: String) { + this.newName = newName.uppercase() + + var oldName: String + oldName = try { // See if it's one of the potion names that changed + PotionTypeMapper.valueOf(newName).oldName + } catch (e: IllegalArgumentException) { + // Name did not change, but remove modifiers + this.newName.replace("STRONG_", "").replace("LONG_", "") + } + this.oldName = oldName + + this.type = getPotionType() + + isStrong = newName.startsWith("STRONG_") + isLong = newName.startsWith("LONG_") + } + + + /** + * Create an instance of [PotionTypeCompat] starting from the old potion name. (<1.20.5) + * Name will be turned into upper case as needed. + * + * @param oldName The old potion type name, e.g. jump. + * @param isStrong Whether the potion is upgraded to amplifier 1 (II). + * @param isLong Whether the potion is extended. + */ + constructor(oldName: String, isStrong: Boolean, isLong: Boolean) { + this.oldName = oldName.uppercase() + + var newName: String? = null + for (mapped in PotionTypeMapper.entries) { + // Check if the old name matches + if (mapped.oldName == this.oldName) { + // Check the 'strong' and 'long' flags + val mappedName = mapped.name + if (isStrong == mappedName.startsWith("STRONG_") && isLong == mappedName.startsWith("LONG_")) { + newName = mappedName + break + } + } + } + + if (newName == null) { // Name did not change + if (isStrong) this.newName = "STRONG_" + this.oldName + else if (isLong) this.newName = "LONG_" + this.oldName + else this.newName = this.oldName + } else this.newName = newName + + this.type = getPotionType() + this.isStrong = isStrong + this.isLong = isLong + } + + + companion object { + /** + * Create an instance of [PotionTypeCompat] based on methods available in currently-running server version. + * @return Instance of [PotionTypeCompat], if found. + */ + @JvmStatic + fun fromPotionMeta(potionMeta: PotionMeta): PotionTypeCompat { + try { // For >=1.20.5 + val potionType = potionMeta.basePotionType + return PotionTypeCompat(potionType!!.name) + } catch (e: NoSuchMethodError) { + val potionData = potionMeta.basePotionData + val potionType = potionData!!.type + return PotionTypeCompat(potionType.name, potionData.isUpgraded, potionData.isExtended) + } + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/Reflector.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/Reflector.kt new file mode 100644 index 00000000..805658a9 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/Reflector.kt @@ -0,0 +1,288 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.reflection + +import kernitus.plugin.OldCombatMechanics.utilities.reflection.type.ClassType +import org.bukkit.Bukkit +import java.lang.reflect.* +import java.util.* +import java.util.function.Function +import java.util.stream.Collectors +import java.util.stream.Stream + +object Reflector { + @JvmField + var version: String? = null + private var majorVersion = 0 + private var minorVersion = 0 + private var patchVersion = 0 + + init { + try { + // Split on the "-" to just get the version information + version = + Bukkit.getServer().bukkitVersion.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + val splitVersion = version!!.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + + majorVersion = splitVersion[0].toInt() + minorVersion = splitVersion[1].toInt() + patchVersion = if (splitVersion.size > 2) { + splitVersion[2].toInt() + } else { + 0 + } + } catch (e: Exception) { + System.err.println("Failed to load Reflector: " + e.message) + } + } + + /** + * Checks if the current server version is newer or equal to the one provided. + * + * @param major the target major version + * @param minor the target minor version. 0 for all + * @param patch the target patch version. 0 for all + * @return true if the server version is newer or equal to the one provided + */ + @JvmStatic + fun versionIsNewerOrEqualTo(major: Int, minor: Int, patch: Int): Boolean { + if (majorVersion < major) return false + if (minorVersion < minor) return false + return patchVersion >= patch + } + + fun getClass(type: ClassType, name: String): Class<*> { + return getClass(type.qualifyClassName(name)) + } + + fun getClass(fqn: String): Class<*> { + try { + return Class.forName(fqn) + } catch (e: ClassNotFoundException) { + throw RuntimeException("Couldn't load class $fqn", e) + } + } + + fun getMethod(clazz: Class<*>, name: String): Method? { + return Arrays.stream(clazz.methods).filter { method: Method -> method.name == name }.findFirst().orElse(null) + } + + fun getMethod(clazz: Class<*>, name: String, parameterCount: Int): Method? { + return Arrays.stream(clazz.methods) + .filter { method: Method -> method.name == name && method.parameterCount == parameterCount }.findFirst() + .orElse(null) + } + + fun getMethod(clazz: Class<*>, returnType: Class<*>?, vararg parameterTypeSimpleNames: String?): Method? { + val typeNames = listOf(*parameterTypeSimpleNames) + return Arrays.stream(clazz.methods).filter { method: Method -> method.returnType == returnType } + .filter { it: Method -> getParameterNames.apply(it) == typeNames }.findFirst().orElse(null) + } + + private val getParameterNames = Function { method: Method -> + Arrays.stream(method.parameters).map { obj: Parameter -> obj.type }.map { obj: Class<*> -> obj.simpleName } + .collect(Collectors.toList()) + } + + fun getMethod(clazz: Class<*>, name: String, vararg parameterTypeSimpleNames: String?): Method? { + val typeNames = listOf(*parameterTypeSimpleNames) + return Stream.concat( + Arrays.stream(clazz.declaredMethods), Arrays.stream(clazz.methods) + ).filter { it: Method -> it.name == name }.filter { it: Method -> getParameterNames.apply(it) == typeNames } + .peek { it: Method -> it.isAccessible = true }.findFirst().orElse(null) + } + + fun getMethodByGenericReturnType(typeVar: TypeVariable<*>, clazz: Class<*>): Method { + for (method in clazz.methods) { + if (method.genericReturnType.typeName == typeVar.name) { + return method + } + } + throw RuntimeException("Method with type $typeVar not found") + } + + + fun invokeMethod(method: Method, handle: Any?, vararg params: Any?): T { + try { + val t = method.invoke(handle, *params) as T + return t + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } catch (e: InvocationTargetException) { + throw RuntimeException(e) + } + } + + /** + * Resolves the given method, caches it and then uses that instance for all future invocations. + * + * + * The returned function just invokes the cached method for a given target. + * + * @param clazz the clazz the method is in + * @param name the name of the method + * @param the type of the handle + * @param the type of the parameter(s) + * @param the type of the method result + * @return a function that invokes the retrieved cached method for its argument + */ + fun memoiseMethodInvocation( + clazz: Class, name: String, vararg argTypes: String? + ): (T, U) -> R { + val method = getMethod(clazz, name, *argTypes) + return { t: T, u: U -> + // If they did not want to send any arguments, should be zero-length array + // This check is necessary cause of varargs, otherwise we get 1 length array of 0-length array + if (u is Array<*> && u.isArrayOf() && (u as Array<*>).isEmpty()) + invokeMethod(method!!, t) + invokeMethod(method!!, t, u) + } + } + + fun getField(clazz: Class<*>, fieldName: String): Field { + try { + val field = clazz.getDeclaredField(fieldName) + field.isAccessible = true + return field + } catch (e: NoSuchFieldException) { + throw RuntimeException(e) + } + } + + fun getFieldByType(clazz: Class<*>, simpleClassName: String): Field { + for (declaredField in clazz.declaredFields) { + if (declaredField.type.simpleName == simpleClassName) { + declaredField.isAccessible = true + return declaredField + } + } + throw RuntimeException("Field with type $simpleClassName not found") + } + + fun getMapFieldWithTypes(clazz: Class<*>, keyType: Class<*>, valueType: Class<*>): Field { + for (field in clazz.declaredFields) { + // Check if the field is a Map + if (MutableMap::class.java.isAssignableFrom(field.type)) { + // Get the generic type of the field + val genericType = field.genericType + if (genericType is ParameterizedType) { + val actualTypeArguments = genericType.actualTypeArguments + // Check if the map's key and value types match the specified classes + if (actualTypeArguments.size == 2 && actualTypeArguments[0] == keyType && actualTypeArguments[1] == valueType) { + field.isAccessible = true + return field + } + } + } + } + throw RuntimeException( + "Map field with key type " + keyType.simpleName + " and value type " + valueType.simpleName + " not found" + ) + } + + @Throws(Exception::class) + fun getFieldValueByType(`object`: Any, simpleClassName: String): Any { + val publicFields = Stream.of(*`object`.javaClass.fields) + val declaredFields = Stream.of(*`object`.javaClass.declaredFields) + val allFields = Stream.concat(publicFields, declaredFields) + + // Find the first field that matches the type name + val matchingField = + allFields.filter { declaredField: Field -> declaredField.type.simpleName == simpleClassName }.findFirst() + .orElseThrow { NoSuchFieldException("Couldn't find field with type " + simpleClassName + " in " + `object`.javaClass) } + + // Make the field accessible and return its value + matchingField.isAccessible = true + return matchingField[`object`] + } + + @JvmStatic + fun getFieldValue(field: Field, handle: Any?): Any { + field.isAccessible = true + try { + return field[handle] + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } + } + + @JvmStatic + fun setFieldValue(field: Field, handle: Any?, value: Any?) { + field.isAccessible = true + try { + field[handle] = value + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } + } + + fun getConstructor(clazz: Class<*>, numParams: Int): Constructor<*>? { + return Stream.concat( + Arrays.stream(clazz.declaredConstructors), Arrays.stream(clazz.constructors) + ).filter { constructor: Constructor<*> -> constructor.parameterCount == numParams } + .peek { it: Constructor<*> -> it.setAccessible(true) }.findFirst().orElse(null) + } + + fun getConstructor(clazz: Class<*>, vararg parameterTypeSimpleNames: String?): Constructor<*>? { + val getParameterNames = Function { constructor: Constructor<*> -> + Arrays.stream(constructor.parameters).map { obj: Parameter -> obj.type } + .map { obj: Class<*> -> obj.simpleName }.collect(Collectors.toList()) + } + val typeNames = listOf(*parameterTypeSimpleNames) + return Stream.concat( + Arrays.stream(clazz.declaredConstructors), Arrays.stream(clazz.constructors) + ).filter { constructor: Constructor<*> -> getParameterNames.apply(constructor) == typeNames } + .peek { it: Constructor<*> -> it.setAccessible(true) }.findFirst().orElse(null) + } + + /** + * Checks if a given class *somehow* inherits from another class + * + * @param toCheck The class to check + * @param inheritedClass The inherited class, it should have + * @return True if `toCheck` somehow inherits from + * `inheritedClass` + */ + fun inheritsFrom(toCheck: Class<*>, inheritedClass: Class<*>): Boolean { + if (inheritedClass.isAssignableFrom(toCheck)) { + return true + } + + for (implementedInterface in toCheck.interfaces) { + if (inheritsFrom(implementedInterface, inheritedClass)) { + return true + } + } + + return false + } + + fun getUnchecked(supplier: UncheckedReflectionSupplier): T { + try { + return supplier.get() + } catch (e: ReflectiveOperationException) { + throw RuntimeException(e) + } + } + + fun doUnchecked(runnable: UncheckedReflectionRunnable) { + try { + runnable.run() + } catch (e: ReflectiveOperationException) { + throw RuntimeException(e) + } + } + + interface UncheckedReflectionSupplier { + @Throws(ReflectiveOperationException::class) + fun get(): T + } + + interface UncheckedReflectionRunnable { + @Throws(ReflectiveOperationException::class) + fun run() + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/SpigotFunctionChooser.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/SpigotFunctionChooser.kt new file mode 100644 index 00000000..d896634e --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/SpigotFunctionChooser.kt @@ -0,0 +1,158 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.reflection + +import kernitus.plugin.OldCombatMechanics.utilities.reflection.SpigotFunctionChooser.ExceptionalFunction.WrappedException + +/** + * Chooses a Spigot API function to use + * Chooses a function to apply based on a test supplier, remembers the choice and only uses the corresponding + * function in the future. + * + * + * The branch to pick is determined during the *first execution of its [.apply] method!*. + * This means that no matter how often the feature branch is invoked, it will never reconsider its choice. + * + * @param the type of the entity to apply the function to + * @param the type of the extra parameter(s). Use [Object] if unused, or list of objects if multiple. + * @param the return type of the function + */ +class SpigotFunctionChooser +/** + * Creates a new [SpigotFunctionChooser], which chooses between two given functions. + * + * @param test the test supplier that will be invoked to choose a branch + * @param trueBranch the branch to pick when then test is true + * @param falseBranch the branch to pick when then test is false + */( + private val test: (T, U) -> Boolean, + private val trueBranch: (T, U) -> R, + private val falseBranch: (T, U) -> R +) { + private var chosen: ((T, U) -> R)? = null + + /** + * Applies the stored action to the given target and chooses what branch to use on the first call. + * + * @param target the target to apply it to + * @param parameters the extra parameters to pass to the function + * @return the result of applying the function to the given target + */ + fun apply(target: T, parameters: U = arrayOfNulls(0) as U): R { + if (chosen == null) { + synchronized(this) { + if (chosen == null) { + chosen = if (test(target, parameters)) trueBranch else falseBranch + } + } + } + return chosen!!.invoke(target, parameters) + } + + fun interface ExceptionalFunction { + /** + * This method is the target of the functional interface where you can + * write logic that might throw an exception. + * + * @param t the first function argument + * @param u the second function argument + * @return the function result + */ + @Throws(Throwable::class) + fun applyWithException(t: T, u: U): R + + /** + * Invokes [applyWithException]. If an exception is thrown, it wraps + * the exception into a [WrappedException]. + * + * @param t the first function argument + * @param u the second function argument + * @return the function result + * @throws WrappedException if any exception is thrown + */ + fun apply(t: T, u: U): R { + return try { + applyWithException(t, u) + } catch (e: Throwable) { + throw WrappedException(e) + } + } + + class WrappedException(cause: Throwable?) : RuntimeException(cause) + } + + companion object { + /** + * Creates a [SpigotFunctionChooser] that uses the success parameter when the action completes without an + * exception and otherwise uses the failure parameter. + * + * + * The action is, per the doc for [SpigotFunctionChooser] only called *once*. + * + * @param action the action to invoke + * @param success the branch to take when no exception occurs + * @param failure the branch to take when an exception occurs + * @param the type of the class containing the method + * @param the type of the parameter(s) + * @param the return type of the method + * @return a [SpigotFunctionChooser] that picks the branch based on whether action threw an exception + */ + private fun onException( + action: ExceptionalFunction, success: (T, U) -> R, failure: (T, U) -> R + ): SpigotFunctionChooser { + return SpigotFunctionChooser( + { t: T, u: U -> + try { + action.apply(t, u) + return@SpigotFunctionChooser true + } catch (e: WrappedException) { + return@SpigotFunctionChooser false + } + }, success, failure + ) + } + + /** + * Calls the Spigot API method if possible, otherwise uses reflection to access same method. + * Useful for API methods that were only added after a certain version. Caches chosen method for performance. + * + * + * Note: 1.16 is last version with Spigot-mapped fields, 1.17 with Spigot-mapped methods. + * + * @param apiCall A reference to the function that should be called + * @param clazz The class containing the function to be accessed via reflection + * @param name The name of the function to be accessed via reflection + * @return A new instance of [SpigotFunctionChooser] + */ + fun apiCompatReflectionCall( + apiCall: ExceptionalFunction, clazz: Class, name: String, vararg argTypes: String? + ): SpigotFunctionChooser { + return onException( + apiCall, + { t, u -> apiCall.apply(t, u) }, + Reflector.memoiseMethodInvocation(clazz, name, *argTypes) + ) + } + + /** + * Calls the Spigot API method if possible, otherwise uses the provided function as a workaround. + * + * + * This should be used to avoid reflection wherever possible, making the plugin more compatible. + * Chosen method is cached for performance. Do not use method references as they are eagerly-bound in Java 8. + * + * + * @param apiCall A reference to the function that should be called + * @param altFunc A function that should instead be called if API method not available. + * @return A new instance of [SpigotFunctionChooser] + */ + fun apiCompatCall( + apiCall: ExceptionalFunction, altFunc: (T, U) -> R + ): SpigotFunctionChooser { + return onException(apiCall, { t, u -> apiCall.apply(t, u) }, altFunc) + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/VersionCompatUtils.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/VersionCompatUtils.kt new file mode 100644 index 00000000..b14458e0 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/VersionCompatUtils.kt @@ -0,0 +1,70 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.reflection + +import org.bukkit.entity.HumanEntity +import org.bukkit.entity.LivingEntity +import java.lang.reflect.Method +import java.util.* + +/** + * Utilities to help with keeping compatibility across multiple versions of the game. + */ +object VersionCompatUtils { + private var cooldownMethod: Method? = null + private val absorptionAmountMethodCache: MutableMap, Method?> = WeakHashMap() + + /** + * Returns a Craft object from the given Spigot object, e.g. CraftPlayer from Player. + * Useful for accessing properties not available through Spigot's API. + * + * @param spigotObject The spigot object to get the handle of, e.g. Player + * @return The Craft object + */ + private fun getCraftHandle(spigotObject: Any): Any? { + return Reflector.invokeMethod(Reflector.getMethod(spigotObject.javaClass, "getHandle")!!, spigotObject) + } + + fun getAttackCooldown(he: HumanEntity): Float { + val craftHumanEntity = getCraftHandle(he) + // public float x(float a), grab by return and param type, cause name changes in each version + if (cooldownMethod == null) // cache this to not search for it every single time + cooldownMethod = Reflector.getMethod( + getCraftHandle(he)!!.javaClass, + Float::class.javaPrimitiveType, "float" + ) + return Reflector.invokeMethod( + cooldownMethod!!, craftHumanEntity, 0.5f + ) + } + + fun getAbsorptionAmount(livingEntity: LivingEntity): Float { + val craftLivingEntity = getCraftHandle(livingEntity) + val leClass: Class<*> = craftLivingEntity!!.javaClass + val absorptionAmountMethod: Method? + // Cache method for each subclass of LivingEntity to not search for it every single time + // Cannot cache for LE itself because method is obtained from each subclass + if (!absorptionAmountMethodCache.containsKey(leClass)) { + absorptionAmountMethod = Reflector.getMethod( + craftLivingEntity.javaClass, "getAbsorptionHearts" + ) + absorptionAmountMethodCache[leClass] = absorptionAmountMethod + } else { + absorptionAmountMethod = absorptionAmountMethodCache[leClass] + } + + // Give useful debugging information in case the method cannot be applied + require(absorptionAmountMethod!!.declaringClass.isAssignableFrom(craftLivingEntity.javaClass)) { + ("Cannot call method '" + absorptionAmountMethod + "' of class '" + absorptionAmountMethod.declaringClass.name + + "' using object '" + craftLivingEntity + "' of class '" + craftLivingEntity.javaClass.name + "' because" + + " object '" + craftLivingEntity + "' is not an instance of '" + absorptionAmountMethod.declaringClass.name + "'") + } + + return Reflector.invokeMethod( + absorptionAmountMethod, craftLivingEntity + ) + } +} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/type/ClassType.java b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/type/ClassType.kt similarity index 51% rename from src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/type/ClassType.java rename to src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/type/ClassType.kt index df6463c8..b6dc4afd 100644 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/utilities/reflection/type/ClassType.java +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/reflection/type/ClassType.kt @@ -3,33 +3,31 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package kernitus.plugin.OldCombatMechanics.utilities.reflection.type; +package kernitus.plugin.OldCombatMechanics.utilities.reflection.type -import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector; - -public enum ClassType { +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector.versionIsNewerOrEqualTo +enum class ClassType { NMS { - @Override - public String qualifyClassName(String partialName){ - if(Reflector.versionIsNewerOrEqualTo(1, 17, 0)){ - return "net.minecraft." + partialName; + override fun qualifyClassName(partialName: String): String { + if (versionIsNewerOrEqualTo(1, 17, 0)) { + return "net.minecraft.$partialName" } // FIXME: Assumes class names are upper case and trims off the preceding package name // entity.foo.Bar.Test // ^^^^^^^^^^ ^^^^^^^^^ // Group 1 Group 2 - String className = partialName.replaceAll("([a-z.]+)\\.([a-zA-Z.]+)", "$2"); + val className = partialName.replace("([a-z.]+)\\.([a-zA-Z.]+)".toRegex(), "$2") - return "net.minecraft.server." + Reflector.getVersion() + "." + className; + return "net.minecraft.server." + Reflector.version + "." + className } }, CRAFTBUKKIT { - @Override - public String qualifyClassName(String partialName){ - return String.format("%s.%s.%s", "org.bukkit.craftbukkit", Reflector.getVersion(), partialName); + override fun qualifyClassName(partialName: String): String { + return String.format("%s.%s.%s", "org.bukkit.craftbukkit", Reflector.version, partialName) } }; - public abstract String qualifyClassName(String partialName); + abstract fun qualifyClassName(partialName: String): String } diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/ModesetListener.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/ModesetListener.kt new file mode 100644 index 00000000..b3542341 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/ModesetListener.kt @@ -0,0 +1,94 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.storage + +import kernitus.plugin.OldCombatMechanics.OCMMain +import kernitus.plugin.OldCombatMechanics.module.OCMModule +import kernitus.plugin.OldCombatMechanics.utilities.Config +import kernitus.plugin.OldCombatMechanics.utilities.Messenger +import org.bukkit.World +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.player.PlayerChangedWorldEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.world.WorldLoadEvent +import org.bukkit.event.world.WorldUnloadEvent +import java.util.* + +/** + * Listens to players changing world / spawning etc. + * and updates modeset accordingly + */ +class ModesetListener(plugin: OCMMain) : OCMModule(plugin, "modeset-listener") { + override fun isEnabled() = true + + override fun isEnabled(world: World) = true + + @EventHandler(priority = EventPriority.LOWEST) + fun onPlayerChangedWorld(event: PlayerChangedWorldEvent) { + val player = event.player + val playerId = player.uniqueId + val playerData = PlayerStorage.getPlayerData(playerId) + val modesetFromName = playerData.getModesetForWorld(event.from.uid) + updateModeset(player, player.world.uid, modesetFromName) + } + + @EventHandler(priority = EventPriority.LOWEST) + fun onPlayerJoin(event: PlayerJoinEvent) { + val player = event.player + updateModeset(player, player.world.uid, null) + } + + @EventHandler(ignoreCancelled = false) + fun onWorldLoad(event: WorldLoadEvent) { + val world = event.world + Config.addWorld(world) + Messenger.info("Loaded configured world " + world.name) + } + + @EventHandler(ignoreCancelled = false) + fun onWorldUnload(event: WorldUnloadEvent) { + val world = event.world + Config.removeWorld(world) + Messenger.info("Unloaded configured world " + world.name) + } + + companion object { + private fun updateModeset(player: Player, worldId: UUID, modesetFromName: String?) { + val playerId = player.uniqueId + val playerData = PlayerStorage.getPlayerData(playerId) + val originalModeset = playerData.getModesetForWorld(worldId) + var modesetName = playerData.getModesetForWorld(worldId) + + // Get modesets allowed in to world + var allowedModesets = Config.worlds[worldId] + if (allowedModesets.isNullOrEmpty()) allowedModesets = Config.getModesets().keys + + // If they don't have a modeset in toWorld yet + if (modesetName == null) { + // Try to use modeset of world they are coming from + modesetName = if (modesetFromName != null && allowedModesets.contains(modesetFromName)) modesetFromName + else // Otherwise, if the from modeset is not allowed, use default for to world + allowedModesets.stream().findFirst().orElse(null) + } + + // If the modeset changed, set and save + if (originalModeset == null || originalModeset != modesetName) { + playerData.setModesetForWorld(worldId, modesetName) + PlayerStorage.setPlayerData(playerId, playerData) + PlayerStorage.scheduleSave() + + Messenger.send( + player, + Config.getConfig().getString("mode-messages.mode-set") + ?: "&4ERROR: &rmode-messages.mode-set string missing", + modesetName ?: "unknown" + ) + } + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerData.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerData.kt new file mode 100644 index 00000000..b01d6071 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerData.kt @@ -0,0 +1,36 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.storage + +import org.bson.Document +import java.util.* + +class PlayerData { + var modesetByWorld: MutableMap = HashMap() + + fun setModesetForWorld(worldId: UUID, modeset: String?) { + modesetByWorld[worldId] = modeset + } + + fun getModesetForWorld(worldId: UUID): String? { + return modesetByWorld[worldId] + } + + companion object { + fun fromDocument(doc: Document): PlayerData { + val playerData = PlayerData() + val modesetByWorldDoc = doc["modesetByWorld"] as Document? + if (modesetByWorldDoc != null) { + for ((key, value) in modesetByWorldDoc) { + val worldId = UUID.fromString(key) + val modeset = value as String + playerData.setModesetForWorld(worldId, modeset) + } + } + return playerData + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerDataCodec.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerDataCodec.kt new file mode 100644 index 00000000..cb1dfcf2 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerDataCodec.kt @@ -0,0 +1,35 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.storage + +import org.bson.BsonReader +import org.bson.BsonWriter +import org.bson.Document +import org.bson.codecs.Codec +import org.bson.codecs.DecoderContext +import org.bson.codecs.DocumentCodec +import org.bson.codecs.EncoderContext + +class PlayerDataCodec : Codec { + override fun encode(writer: BsonWriter, value: PlayerData, encoderContext: EncoderContext) { + val document = Document() + val modesetByWorldDoc = Document() + for ((key, value1) in value.modesetByWorld) { + modesetByWorldDoc[key.toString()] = value1 + } + document["modesetByWorld"] = modesetByWorldDoc + DocumentCodec().encode(writer, document, encoderContext) + } + + override fun getEncoderClass(): Class { + return PlayerData::class.java + } + + override fun decode(reader: BsonReader, decoderContext: DecoderContext): PlayerData { + val document = DocumentCodec().decode(reader, decoderContext) + return PlayerData.Companion.fromDocument(document) + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerStorage.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerStorage.kt new file mode 100644 index 00000000..8d8fef53 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/utilities/storage/PlayerStorage.kt @@ -0,0 +1,135 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.utilities.storage + +import kernitus.plugin.OldCombatMechanics.OCMMain +import org.bson.* +import org.bson.codecs.* +import org.bson.codecs.configuration.CodecRegistries +import org.bson.codecs.configuration.CodecRegistry +import org.bson.io.BasicOutputBuffer +import org.bukkit.Bukkit +import org.bukkit.scheduler.BukkitTask +import java.io.File +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import java.util.logging.Level + +/** + * Stores data associated with players to disk, persisting across server restarts. + */ +object PlayerStorage { + private var plugin: OCMMain? = null + private var dataFilePath: Path? = null + private var documentCodec: DocumentCodec? = null + private var data: Document? = null + private val saveTask = AtomicReference() + private var codecRegistry: CodecRegistry? = null + + fun initialise(plugin: OCMMain) { + PlayerStorage.plugin = plugin + dataFilePath = Paths.get(plugin.dataFolder.toString() + File.separator + "players.bson") + + codecRegistry = CodecRegistries.fromRegistries( + CodecRegistries.fromCodecs(DocumentCodec()), // Explicitly provide a DocumentCodec + CodecRegistries.fromCodecs(PlayerDataCodec()), + CodecRegistries.fromProviders(BsonValueCodecProvider(), ValueCodecProvider()) // For BSON values + ) + + documentCodec = DocumentCodec(codecRegistry) + + data = loadData() + + saveTask.set(null) + } + + private fun loadData(): Document { + if (Files.notExists(dataFilePath)) return Document() + + try { + val data = Files.readAllBytes(dataFilePath) + val reader: BsonReader = BsonBinaryReader(ByteBuffer.wrap(data)) + return documentCodec!!.decode(reader, DecoderContext.builder().build()) + } catch (e: IOException) { + plugin!!.logger.log(Level.SEVERE, "Error loading player data", e) + } + return Document() + } + + fun scheduleSave() { + // Schedule a task for later, if there isn't one already scheduled + saveTask.compareAndSet( + null, + Bukkit.getScheduler().runTaskLaterAsynchronously(plugin!!, Runnable { + instantSave() + saveTask.set(null) + }, 2400L) // Save after 2 minutes + ) + } + + fun instantSave() { + val outputBuffer = BasicOutputBuffer() + val writer: BsonWriter = BsonBinaryWriter(outputBuffer) + documentCodec!!.encode(writer, data, EncoderContext.builder().isEncodingCollectibleDocument(true).build()) + writer.flush() + + try { + Files.write(dataFilePath, outputBuffer.toByteArray()) + } catch (e: IOException) { + plugin!!.logger.log(Level.SEVERE, "Error saving player data", e) + } finally { + outputBuffer.close() + } + } + + @JvmStatic + fun getPlayerData(uuid: UUID): PlayerData { + val playerDoc = data!![uuid.toString()] as Document? + if (playerDoc == null) { + val playerData = PlayerData() + setPlayerData(uuid, playerData) + scheduleSave() + return playerData + } + val bsonDocument: BsonDocument = BsonDocumentWrapper(playerDoc, documentCodec) + return codecRegistry!!.get(PlayerData::class.java) + .decode(bsonDocument.asBsonReader(), DecoderContext.builder().build()) + } + + @JvmStatic + fun setPlayerData(uuid: UUID, playerData: PlayerData) { + // Create a BsonDocumentWriter to hold the encoded data + val writer = BsonDocumentWriter(BsonDocument()) + + // Get the PlayerDataCodec from the CodecRegistry + val playerDataCodec = codecRegistry!!.get(PlayerData::class.java) as PlayerDataCodec + + // Encode the PlayerData object to the writer + playerDataCodec.encode(writer, playerData, EncoderContext.builder().isEncodingCollectibleDocument(true).build()) + + // Retrieve the BsonDocument + val bsonDocument = writer.document + + // Convert the BsonDocument to a Document + val document = Document() + bsonDocument.forEach { (key: String, value: BsonValue) -> + document[key] = if (value.isDocument) Document(value.asDocument()) else value + } + + // Put the Document into your data map + data!![uuid.toString()] = document + } +} \ No newline at end of file diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/enchantments/EnchantmentCompat.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/enchantments/EnchantmentCompat.kt new file mode 100644 index 00000000..dd10d696 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/enchantments/EnchantmentCompat.kt @@ -0,0 +1,49 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.versions.enchantments + +import org.bukkit.enchantments.Enchantment + +enum class EnchantmentCompat(oldName: String) { + UNBREAKING("DURABILITY"), + PROTECTION("PROTECTION_ENVIRONMENTAL"), + FIRE_PROTECTION("PROTECTION_FIRE"), + BLAST_PROTECTION("PROTECTION_EXPLOSIONS"), + PROJECTILE_PROTECTION("PROTECTION_PROJECTILE"), + FEATHER_FALLING("PROTECTION_FALL"), + SMITE("DAMAGE_UNDEAD"), + BANE_OF_ARTHROPODS("DAMAGE_ARTHROPODS"), + SHARPNESS("DAMAGE_ALL"); + + // Try loading the new name first + // This only happens once per enum name + private var enchantment: Enchantment = Enchantment.getByName(name) + ?: Enchantment.getByName(oldName) + ?: throw IllegalStateException("PotionEffectType not found for: $name or $oldName") + + fun get(): Enchantment { + return enchantment + } + + companion object { + /** + * Gets correct [Enchantment] for currently-running server version given new name. + * + * @param newName The [Enchantment] >=1.20.6 name + * @return The [Enchantment] for the currently-running server version, or null if not found. + */ + fun fromNewName(newName: String): Enchantment? { + return try { + // See if new name needs mapping to old + valueOf(newName.uppercase()) + .get() + } catch (e: IllegalArgumentException) { + // Otherwise use new name directly + Enchantment.getByName(newName) + } + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/DualVersionedMaterial.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/DualVersionedMaterial.kt new file mode 100644 index 00000000..63d40f88 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/DualVersionedMaterial.kt @@ -0,0 +1,67 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.versions.materials + +import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import java.util.function.Supplier + +/** + * A material that has a version for before 1.13 and after 1.13. + */ +class DualVersionedMaterial +/** + * Creates a new dual versioned material. + * + * @param oldItem the item supplier for the old version + * @param newItem the item supplier for the new version + */(private val oldItem: Supplier, private val newItem: Supplier) : + VersionedMaterial { + override fun newInstance(): ItemStack { + return itemSupplier.get() + } + + @Suppress("deprecation") + override fun isSame(other: ItemStack): Boolean { + val baseInstance = newInstance() + + // items do not differ in more than those two things + return baseInstance.type == other.type && baseInstance.durability == other.durability + } + + private val itemSupplier: Supplier + get() = if (Reflector.versionIsNewerOrEqualTo( + 1, + 13, + 0 + ) + ) newItem else oldItem + + override fun toString(): String { + return ("DualVersionedMaterial{" + + "picked=" + (if (itemSupplier === newItem) "new" else "old") + + ", item=" + newInstance() + + '}') + } + + companion object { + /** + * Returns a new [DualVersionedMaterial] based on the material names. + * + * @param nameOld the old name + * @param nameNew the new name + * @return a dual versioned material using the supplied names + */ + fun ofMaterialNames(nameOld: String, nameNew: String): VersionedMaterial { + return DualVersionedMaterial(fromMaterial(nameOld), fromMaterial(nameNew)) + } + + private fun fromMaterial(name: String): Supplier { + return Supplier { ItemStack(Material.matchMaterial(name)!!) } + } + } +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/MaterialRegistry.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/MaterialRegistry.kt new file mode 100644 index 00000000..1937f3e1 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/MaterialRegistry.kt @@ -0,0 +1,26 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.versions.materials + +import org.bukkit.Material +import org.bukkit.inventory.ItemStack + +/** + * Contains Materials that are different in > 1.13 and < 1.13. + */ +object MaterialRegistry { + @JvmField + var LAPIS_LAZULI: VersionedMaterial = DualVersionedMaterial( + { ItemStack(Material.matchMaterial("INK_SACK")!!, 1, 4.toShort()) }, + { ItemStack(Material.matchMaterial("LAPIS_LAZULI")!!) } + ) + + @JvmField + var ENCHANTED_GOLDEN_APPLE: VersionedMaterial = DualVersionedMaterial( + { ItemStack(Material.GOLDEN_APPLE, 1, 1.toShort()) }, + { ItemStack(Material.valueOf("ENCHANTED_GOLDEN_APPLE")) } + ) +} diff --git a/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/NameListVersionedMaterial.kt b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/NameListVersionedMaterial.kt new file mode 100644 index 00000000..e3c31c83 --- /dev/null +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/NameListVersionedMaterial.kt @@ -0,0 +1,52 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package kernitus.plugin.OldCombatMechanics.versions.materials + +import org.bukkit.Material +import org.bukkit.inventory.ItemStack + +/** + * A material that tries each name in a given list until it finds a working material. + */ +class NameListVersionedMaterial private constructor(private val finalMaterial: Material) : VersionedMaterial { + override fun newInstance(): ItemStack { + return ItemStack(finalMaterial) + } + + override fun isSame(other: ItemStack): Boolean { + return other.type == finalMaterial + } + + companion object { + /** + * Returns a new [VersionedMaterial] that picks the first working one from a list of names. + * + * @param names the names of the materials + * @return the versioned material + * @throws IllegalArgumentException if no material was valid + */ + fun ofNames(vararg names: String): VersionedMaterial { + for (name in names) { + var material = Material.matchMaterial(name) + if (material != null) { + return NameListVersionedMaterial(material) + } + + material = Material.matchMaterial(name, true) + if (material != null) { + return NameListVersionedMaterial(material) + } + } + + throw IllegalArgumentException( + "Could not find any working material, tried: " + java.lang.String.join( + ",", + *names + ) + "." + ) + } + } +} diff --git a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/VersionedMaterial.java b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/VersionedMaterial.kt similarity index 71% rename from src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/VersionedMaterial.java rename to src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/VersionedMaterial.kt index 78913b96..2e965454 100644 --- a/src/main/java/kernitus/plugin/OldCombatMechanics/versions/materials/VersionedMaterial.java +++ b/src/main/kotlin/kernitus/plugin/OldCombatMechanics/versions/materials/VersionedMaterial.kt @@ -3,21 +3,20 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package kernitus.plugin.OldCombatMechanics.versions.materials; +package kernitus.plugin.OldCombatMechanics.versions.materials -import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemStack /** * A material in different versions. */ -public interface VersionedMaterial { - +interface VersionedMaterial { /** * Creates a new item stack. * * @return the created item stack */ - ItemStack newInstance(); + fun newInstance(): ItemStack /** * Returns whether the item stack is of this material. @@ -25,5 +24,5 @@ public interface VersionedMaterial { * @param other the itemstack * @return true if the item stack is of this material. */ - boolean isSame(ItemStack other); + fun isSame(other: ItemStack): Boolean } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a1c661a9..2fafcbcb 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -516,24 +516,6 @@ old-burn-delay: # How long, in ticks, entities should be on fire for after not being in direct contact anymore fire-ticks: 120 -disable-projectile-randomness: - # This is to remove projectile randomness while firing arrows with a bow - # Or to remove effects of velocity when player is running and launching potions etc - # This is actually a very old feature and has been in the game for quite some time - enabled: false - # What projectiles are affected e.g. arrow, splash_potion, snowball, egg, fishing_hook - projectile-types: [ arrow ] - # This is the threshold between projectiles' (X,Z) values before they're considered the same and straightened - # This value is only useful for multishot. The default of 0.1 works at all but extremely shallow angles, - # where arrows end up bunched together. Set to 1 if you want multishots to all follow the same path. - epsilon: 0.1 - -disable-bow-boost: - # This is to stop players from boosting themselves forward by hitting themselves - # while running with a punch II arrow from their bow - # This module simply stops them from hitting themselves with arrows entirely - enabled: false - disable-attack-sounds: # Disables attack sounds that were added with 1.9+ # Requires ProtocolLib @@ -576,4 +558,4 @@ debug: enabled: false # DO NOT CHANGE THIS NUMBER AS IT WILL RESET YOUR CONFIG -config-version: 66 +config-version: 67