29 Commits

Author SHA1 Message Date
c4e29ff728 Update for 1.20.3
- Added verbose entry to the Player configuration files. Player configuration files will need to be regenerated. Please make sure to make backups of all configurations before regenerating configuration files.
- Fixed a bug with the hide feature; now, any entity within a 25 block radius will automatically lose target of the player when their luck activates while sneaking.
- Removed the vein-mining feature as it is unstable and non-functional. A fix and/or replacement will be implemented in a future update.
- Updated supported version to 1.20.3
- Adjusted Logging methods.
2023-12-07 20:38:47 -06:00
32a9435e30 Fixed stream error when triggering OreVein effect 2022-06-15 11:23:54 -05:00
d282c2e982 Revert "FeelingLucky v1.3.0"
This reverts commit 0ad60075da.
2022-06-15 11:17:04 -05:00
0ad60075da FeelingLucky v1.3.0
This update will entail providing spigot support.
This is currently incomplete.
2022-06-15 09:35:35 -05:00
661b7bd6a7 Removed deprecated "new Double" call. 2022-06-15 08:38:42 -05:00
639bf09e48 Merge branch 'main' of https://github.com/SimplexDevelopment/FeelingLucky 2022-06-15 08:34:22 -05:00
8b3486a269 Update v1.2.1
Changed version to 1.2.1
Added codacy analysis workflow.
2022-06-15 08:34:15 -05:00
94b6067f97 Update README.md 2022-06-15 08:30:09 -05:00
0b07bd9da2 Minor Update 1.2.1
Adjusted the Luck class to reflect:
- SplittableRandom has been replaced with SecureRandom in favor of entropy-based pseudorandom calculations compared to pseudorandom calculations based off the system time.
- Adjusted the quickRNG to factor in whether the user has the luck potion effect, and to just apply the multiplier regardless of whether it is the default value.
- Also adjusted the values, as the original value still remained at 1024, whereas the randomized number criteria was a percentage of 100. Both the input value and the randomized number criteria now are percentages of 100, based off a total of 1024 possible points.
2022-06-15 08:28:53 -05:00
400687733f Update README.md 2022-06-14 12:30:43 -05:00
c7a168ede1 Update README.md 2022-06-14 12:25:33 -05:00
fda004a3c8 Update README.md 2022-06-14 01:52:21 -05:00
0c82515f43 Update Release 1.2.0
Added a command to regenerate the configuration file.

This command can only be used from console.
2022-06-14 01:51:45 -05:00
c3d781f5b6 FeelingLucky v1.2.0 RC01 2022-06-12 00:12:06 -05:00
e6fe9e904e FeelingLucky v1.0 RC01
Added some more features, this will be now the full official release; this commit is release candidate 1.

Changelog:
- Added HideCheck, which will break the tracking of any mobs targeting the player.
- Added JumpBoost, which adds a little extra height to your jumps.
- Modified OreVein as it previously scanned for all ore types rather than the relative mined ore type.
2022-06-12 00:10:56 -05:00
c383b2c546 Merge pull request #2 from allinkdev/ver/1.19
Update version from "1.18.2" to "1.19"
2022-06-11 20:06:26 -05:00
94b9e12f45 Change version from "1.18.2" to "1.19" 2022-06-10 14:13:48 +01:00
eb80523edc Update README.md 2022-06-08 00:39:45 -05:00
355b612732 Moved README up to Parent Directory 2022-06-08 00:39:13 -05:00
67734f3f89 Added README.md 2022-06-08 00:34:09 -05:00
4232842749 Merge branch 'main' of https://github.com/SimplexDevelopment/FeelingLucky 2022-05-20 16:54:37 -05:00
010fd76031 FeelingLucky v1.0.0 Release Clean Up
Cleaned up a bunch of stuff and made the luck stat unique to the plugin rather than using the values provided by minecraft.
2022-05-20 16:54:28 -05:00
c50b222586 Create codeql-analysis.yml 2022-05-20 16:20:24 -05:00
17f83bd9f2 FeelingLucky v1.0 RELEASE 2022-05-20 15:41:34 -05:00
10d7a4ed98 Minor Functionality Changes
- Modified SpecialRabbitsFoot
- Improved functionality of some code interactions
2022-05-17 13:34:25 -05:00
73e5be91eb Critical Bugfix
Fixed an issue where plugin was loading player configurations from ./plugins/FeelingLucky instead of ./plugins/FeelingLucky/players
2022-05-16 20:59:49 -05:00
07c4e5d50c Update LuckCMD.java
- Implemented PluginIdentifiableCommand
- Changed the way the command is registered in the command map.
2022-05-10 12:50:59 -05:00
e13ca55adf Version Change 2022-05-08 22:56:52 -05:00
fbd8d10461 Added metrics 2022-04-26 14:37:24 -05:00
38 changed files with 1951 additions and 354 deletions

32
.github/workflows/codacy-analysis.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Codacy Security Scan
on:
push:
branches: [ "master", "main" ]
pull_request:
branches: [ "master", "main" ]
jobs:
codacy-security-scan:
name: Codacy Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@main
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@master
with:
output: results.sarif
format: sarif
# Adjust severity of non-security issues
gh-code-scanning-compat: true
# Force 0 exit code to allow SARIF file generation
# This will handover control about PR rejection to the GitHub side
max-allowed-issues: 2147483647
# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@main
with:
sarif_file: results.sarif

77
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,77 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '25 16 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Codacy Coverage Reporter
uses: codacy/codacy-coverage-reporter-action@v1.3.0

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# <center>FeelingLucky v1.2.0 - A luck driven mechanics plugin.</center>
## <center><u><span style="color:blue">Plugin Description:</u></center>
### <center><u><span style="color:cyan">For All Users:</u></center>
<b>FeelingLucky</b> is a mechanics plugin designed to expand upon the Luck attribute which Minecraft provides.
Each user is assigned a tangible Luck stat, which can be viewed by using <b><span style="color:violet">/luck info</color></b>.
Users can <i>increase</i> their luck stat by using a rabbit's foot,
or increase their luck and their luck multiplier by consuming a special rabbits foot purchased from a Butcher villager.
<b><span style="color:red">Beware</b> though, if you take damage from guardian lasers or a witch's potion,
there's a chance your luck will <i>decrease</i> instead.
### <center><u><span style="color:pink">For Administrators:</u></center>
Admins can modify values in the configuration file, as well as modify individual user's luck stat.
Admins can set, reset, add to, and take from player's luck stat.
Admins can also reload the main configuration, as well as individual and all player configurations.
For this, the command is <b><span style="color:violet">/luck reload -m</color></b> for the main config,
<b><span style="color:violet">/luck reload</color></b> to reload all player configurations, and <b><span style="color:violet">/luck reload -p <i>PLAYER_NAME</i></span></b> to reload individual player configuration files.
Server owners and/or individuals with console access can run /rgc to regenerate the main configuration file in the case that there are values missing, corrupted, or invalid.
## <center><u><span style="color:blue">Server Requirements:</u></center>
In order to run <b>FeelingLucky</b> v<b>1.1.0</b>, the latest version of Paper or Spigot is required.
#### <center><span style="color:red">Note: Paper is REQUIRED for this plugin to run. Spigot is not supported, however Spigot support is currently in progress.</center></span>
### <center>Note: If you are migrating from an Alpha build, the plugin configuration folder will need to be regenerated.</center>

View File

@ -3,22 +3,16 @@ plugins {
} }
group = 'io.github.simplex' group = 'io.github.simplex'
version = 'Beta-1.0-RC01' version = '1.3.0'
repositories { repositories {
mavenCentral() mavenCentral()
maven { maven { url = uri("https://s01.oss.sonatype.org/content/groups/public/") }
name = 'papermc-repo' maven { url = uri("https://papermc.io/repo/repository/maven-public/")}
url = 'https://papermc.io/repo/repository/maven-public/'
}
maven {
name = 'sonatype'
url = 'https://s01.oss.sonatype.org/content/groups/public/'
}
} }
dependencies { dependencies {
compileOnly 'io.papermc.paper:paper-api:1.18.1-R0.1-SNAPSHOT' compileOnly("io.papermc.paper:paper-api:1.20.3-R0.1-SNAPSHOT")
} }
def targetJavaVersion = 17 def targetJavaVersion = 17

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -1,15 +1,14 @@
package io.github.simplex.api; package io.github.simplex.api;
import org.bukkit.attribute.Attribute; import java.io.Serializable;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.io.Serializable; public interface LuckContainer extends Serializable
{
public interface LuckContainer extends Serializable { boolean isVerbose();
Attribute asAttribute(); void setVerbose(boolean verbose);
double getNumber();
boolean isMatch(double number); boolean isMatch(double number);

View File

@ -1,21 +1,24 @@
package io.github.simplex.lib; package io.github.simplex.lib;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike;
public enum Messages { public enum Messages
{
NOT_FROM_CONSOLE(MiniComponent.err("This command may only be used in game.")), NOT_FROM_CONSOLE(MiniComponent.err("This command may only be used in game.")),
NO_PERMISSION(MiniComponent.err("You do not have permission to use this command.")), NO_PERMISSION(MiniComponent.err("You do not have permission to use this command.")),
NO_PLAYER(MiniComponent.warn("That player cannot be found.")), NO_PLAYER(MiniComponent.warn("That player cannot be found.")),
OUT_OF_BOUNDS(MiniComponent.err("Number must be between -1024.0 and 1024.0")); OUT_OF_BOUNDS(MiniComponent.err("Number must be between -1024.0 and 1024.0"));
private final Component message; private final ComponentLike message;
Messages(Component message) { Messages(ComponentLike message)
{
this.message = message; this.message = message;
} }
public Component get() { public ComponentLike get()
{
return message; return message;
} }
} }

View File

@ -1,10 +1,12 @@
package io.github.simplex.lib; package io.github.simplex.lib;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
public class MiniComponent { public class MiniComponent {
private final String content; private final String content;
@ -21,17 +23,17 @@ public class MiniComponent {
} }
@Contract("_ -> new") @Contract("_ -> new")
public static Component info(String content) { public static ComponentLike info(String content) {
return new MiniComponent(content).color(ChatColor.GREEN).send(); return new MiniComponent(content).color(ChatColor.GREEN).send();
} }
@Contract("_ -> new") @Contract("_ -> new")
public static Component warn(String content) { public static ComponentLike warn(String content) {
return new MiniComponent(content).color(ChatColor.YELLOW).decorate(TextDecoration.ITALIC).send(); return new MiniComponent(content).color(ChatColor.YELLOW).decorate(TextDecoration.ITALIC).send();
} }
@Contract("_ -> new") @Contract("_ -> new")
public static Component err(String content) { public static ComponentLike err(String content) {
return new MiniComponent(content).color(ChatColor.RED).decorate(TextDecoration.BOLD).send(); return new MiniComponent(content).color(ChatColor.RED).decorate(TextDecoration.BOLD).send();
} }
@ -45,7 +47,7 @@ public class MiniComponent {
return this; return this;
} }
public Component send() { public @Nullable Component send() {
if (color == null) { if (color == null) {
if (decoration == null) return Component.empty().content(content); if (decoration == null) return Component.empty().content(content);

View File

@ -1,89 +1,116 @@
package io.github.simplex.luck; package io.github.simplex.luck;
import io.github.simplex.luck.listener.AbstractListener; import io.github.simplex.luck.listener.AbstractListener;
import io.github.simplex.luck.util.Logs;
import io.github.simplex.luck.util.SneakyWorker; import io.github.simplex.luck.util.SneakyWorker;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File; import java.io.File;
import java.nio.file.Files; import java.util.HashMap;
import java.nio.file.Path; import java.util.Map;
import java.util.ArrayList; import org.bukkit.configuration.file.YamlConfiguration;
import java.util.List; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public class Config extends YamlConfiguration { public class Config extends YamlConfiguration
private final FeelingLucky plugin; {
private final List<String> configEntries = new ArrayList<>() {{ private final Map<String, Object> configEntries = new HashMap<>()
add("high_rarity_chance"); {{
add("medium_rarity_chance"); put("high_rarity_chance", 512.0);
add("low_rarity_chance"); put("medium_rarity_chance", 128.0);
add("block_drops"); put("low_rarity_chance", 64.0);
add("bonemeal"); put("block_drops", "LOW");
add("cheat_death"); put("bonemeal", "MED");
add("enchanting"); put("cheat_death", "MED");
add("experience"); put("enchanting", "HIGH");
add("item_drops"); put("experience", "HIGH");
add("random_effect"); put("give_damage", "LOW");
add("restore_hunger"); put("hide_check", "MED");
add("take_damage"); put("item_drops", "LOW");
add("unbreakable"); put("jump_boost", "MED");
put("ore_vein", "HIGH");
put("random_effect", "HIGH");
put("restore_hunger", "NONE");
put("take_damage", "MED");
put("unbreakable", "HIGH");
}}; }};
private File configFile; private File configFile;
public Config(FeelingLucky plugin) { public Config(FeelingLucky plugin)
this.plugin = plugin; {
File dataFolder = plugin.getDataFolder(); File dataFolder = plugin.getDataFolder();
if (!dataFolder.exists()) dataFolder.mkdirs(); if (dataFolder.mkdirs())
{
plugin.getLogger().info("Created new data folder. Writing new configuration file...");
plugin.saveResource("config.yml", true);
}
File configFile = new File(dataFolder, "config.yml"); File configFile = new File(dataFolder, "config.yml");
if (!configFile.exists()) { if (!configFile.exists())
SneakyWorker.sneakyTry(configFile::createNewFile); {
plugin.getLogger().info("No configuration file exists. Creating a new one...");
plugin.saveResource("config.yml", true); plugin.saveResource("config.yml", true);
} }
this.configFile = configFile; this.configFile = configFile;
load();
if (validateIntegrity()) { if (validateIntegrity(this.configFile))
File newFile = new File(plugin.getDataFolder(), "config.yml"); {
SneakyWorker.sneakyTry(() -> {
Files.delete(Path.of(this.configFile.getPath()));
newFile.createNewFile();
plugin.saveResource("config.yml", true);
});
this.configFile = newFile;
load(); load();
} }
else
{
configEntries.forEach(super::set);
Logs.warn("Your configuration file is missing keys. " +
"\nPlease use /rgc in the console to regenerate the config file. " +
"\nAlternatively, delete the config.yml and restart your server. " +
"\nIt is safe to ignore this, as default values will be used." +
"\nHowever, it is highly recommended to regenerate the configuration.");
}
} }
public void save() { public void save()
{
SneakyWorker.sneakyTry(() -> save(configFile)); SneakyWorker.sneakyTry(() -> save(configFile));
} }
public void load() { public void load()
{
SneakyWorker.sneakyTry(() -> load(configFile)); SneakyWorker.sneakyTry(() -> load(configFile));
} }
public void reload() { public void reload()
{
save(); save();
load(); load();
} }
public boolean validateIntegrity() { public boolean validateIntegrity(@NotNull File fromDisk)
for (String key : getKeys(false)) { {
if (!configEntries.contains(key)) { YamlConfiguration disk = YamlConfiguration.loadConfiguration(fromDisk);
plugin.getLogger().severe("The contents of your configuration file is corrupted! Regenerating a new configuration file..."); if (disk.getKeys(true).isEmpty())
return true; {
}
}
return false; return false;
} }
public AbstractListener.Rarity getRarity(String name) { boolean result = true;
for (String key : configEntries.keySet())
{
if (!disk.getKeys(false).contains(key))
{
if (result)
result = false;
}
}
return result;
}
public AbstractListener.Rarity getRarity(String name)
{
return AbstractListener.Rarity.valueOf(getString(name)); return AbstractListener.Rarity.valueOf(getString(name));
} }
public double getChance(String path) { public double getChance(String path)
{
return getDouble(path); return getDouble(path);
} }
} }

View File

@ -4,31 +4,40 @@ import io.github.simplex.luck.listener.*;
import io.github.simplex.luck.player.PlayerConfig; import io.github.simplex.luck.player.PlayerConfig;
import io.github.simplex.luck.player.PlayerHandler; import io.github.simplex.luck.player.PlayerHandler;
import io.github.simplex.luck.util.LuckCMD; import io.github.simplex.luck.util.LuckCMD;
import io.github.simplex.luck.util.SneakyWorker; import io.github.simplex.luck.util.RegenerateConfigCMD;
import org.bukkit.plugin.java.JavaPlugin; import io.github.simplex.luck.util.SpecialFootItem;
import org.jetbrains.annotations.NotNull; import io.github.simplex.metrics.Metrics;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandMap;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
public final class FeelingLucky extends JavaPlugin { public final class FeelingLucky extends JavaPlugin
{
private final Map<UUID, PlayerConfig> configMap = new HashMap<>(); private final Map<UUID, PlayerConfig> configMap = new HashMap<>();
private final File playerDirectory = new File(getDataFolder(), "players");
private final SpecialFootItem specialFootItem = new SpecialFootItem();
private final ChatType.Bound bind = ChatType.CHAT.bind(Component.text(getName()));
private PlayerHandler handler; private PlayerHandler handler;
private Config config; private Config config;
public Map<UUID, PlayerConfig> getConfigMap() { public Map<UUID, PlayerConfig> getConfigMap()
{
return configMap; return configMap;
} }
@Override @Override
public void onEnable() { public void onEnable()
getLogger().info("Initializing the PlayerHandler..."); {
getLogger().info("Initializing metrics...");
new Metrics(this, 15054);
getLogger().info("Metrics loaded. Initializing the PlayerHandler...");
handler = new PlayerHandler(this); handler = new PlayerHandler(this);
getLogger().info("Initialization complete! Attempting to register the Listeners..."); getLogger().info("Initialization complete! Attempting to register the Listeners...");
registerListeners(); registerListeners();
@ -36,15 +45,17 @@ public final class FeelingLucky extends JavaPlugin {
loadPlayerConfigurations(); loadPlayerConfigurations();
getLogger().info("Attempting to load the main configuration..."); getLogger().info("Attempting to load the main configuration...");
config = new Config(this); config = new Config(this);
getLogger().info("Main Config loaded successfully! Attempting to load the Luck command..."); getLogger().info("Main Config loaded successfully! Loading commands...");
new LuckCMD(this); new LuckCMD(this);
getLogger().info("Successfully loaded the Luck command!"); new RegenerateConfigCMD(this);
getLogger().info("Successfully loaded all commands!");
getLogger().info("Successfully initialized!"); getLogger().info("Successfully initialized!");
} }
@Override @Override
public void onDisable() { public void onDisable()
{
getLogger().info("Saving all player configurations..."); getLogger().info("Saving all player configurations...");
configMap.values().forEach(PlayerConfig::save); configMap.values().forEach(PlayerConfig::save);
getLogger().info("Complete! Saving the main config..."); getLogger().info("Complete! Saving the main config...");
@ -52,42 +63,78 @@ public final class FeelingLucky extends JavaPlugin {
getLogger().info("Complete! Goodbye! :)"); getLogger().info("Complete! Goodbye! :)");
} }
private void loadPlayerConfigurations() { private void loadPlayerConfigurations()
File[] files = getDataFolder().listFiles(); {
if (files != null) { if (!playerDirectory.exists())
Arrays.stream(files).forEach(file -> { {
getLogger().info("No directory exists. Creating...");
playerDirectory.mkdirs();
getLogger().info("Created new directory \"FeelingLucky/players\".");
return;
}
File[] files = playerDirectory.listFiles();
if (files != null)
{
Arrays.stream(files).forEach(file ->
{
UUID uuid = UUID.fromString(file.getName().split("\\.")[0]); UUID uuid = UUID.fromString(file.getName().split("\\.")[0]);
configMap.put(uuid, PlayerConfig.loadFrom(this, file)); configMap.put(uuid, PlayerConfig.initFrom(this, file));
}); });
configMap.forEach((u, pc) -> pc.load()); configMap.forEach((u, pc) -> pc.load());
getLogger().info("Successfully loaded all configurations!"); getLogger().info("Successfully loaded all configurations!");
} else { }
else
{
getLogger().info("There are no player configurations to load."); getLogger().info("There are no player configurations to load.");
} }
} }
private void registerListeners() { private void registerListeners()
try { {
Class<?>[] listeners = SneakyWorker.getClasses(AbstractListener.class.getPackage().getName()); new BlockDrops(this);
Arrays.stream(listeners).forEach(l -> { new BonemealFullCrop(this);
if (AbstractListener.class.isAssignableFrom(l)) { new CheatDeath(this);
if (l.equals(AbstractListener.class)) return; new EnchantmentBoost(this);
new ExpBoost(this);
SneakyWorker.sneakyTry(() -> l.getDeclaredConstructor(FeelingLucky.class).newInstance(this)); new GiveDamage(this);
} new HideCheck(this);
}); new IllOmen(this);
} catch (IOException | ClassNotFoundException ex) { new ItemDrops(this);
getLogger().severe(ex.getMessage()); new JumpBoost(this);
} // new OreVein(this); (Currently unstable & unsafe).
new PlayerListener(this);
new RandomEffect(this);
new RestoreHunger(this);
new TakeDamage(this);
new UnbreakableTool(this);
new VillagerInventory(this);
} }
public PlayerHandler getHandler() { public PlayerHandler getHandler()
{
return handler; return handler;
} }
@Override @Override
@NotNull @NotNull
public Config getConfig() { public Config getConfig()
{
return config; return config;
} }
public SpecialFootItem getFoot()
{
return specialFootItem;
}
public CommandMap getCommandMap()
{
return getServer().getCommandMap();
}
public ChatType.Bound bind()
{
return bind;
}
} }

View File

@ -1,29 +1,31 @@
package io.github.simplex.luck.listener; package io.github.simplex.luck.listener;
import io.github.simplex.luck.Config;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.PlayerHandler; import io.github.simplex.luck.player.PlayerHandler;
import net.kyori.adventure.audience.Audience;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
public abstract class AbstractListener implements Listener { public abstract class AbstractListener implements Listener {
protected final FeelingLucky plugin; protected final FeelingLucky plugin;
protected final Config config;
public AbstractListener(FeelingLucky plugin) { public AbstractListener(FeelingLucky plugin) {
this.plugin = plugin; this.plugin = plugin;
this.config = plugin.getConfig();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
protected PlayerHandler getHandler() { protected PlayerHandler getHandler() {
return plugin.getHandler(); return plugin.getHandler();
} }
public void register(AbstractListener listener) {
plugin.getServer().getPluginManager().registerEvents(listener, plugin);
}
public boolean doesQualify(String name, double luck) { public boolean doesQualify(String name, double luck) {
return switch (config.getRarity(name)) { return switch (plugin.getConfig().getRarity(name)) {
case HIGH -> luck > config.getChance("high_rarity_chance"); case HIGH -> luck > plugin.getConfig().getChance("high_rarity_chance");
case MED -> luck > config.getChance("medium_rarity_chance"); case MED -> luck > plugin.getConfig().getChance("medium_rarity_chance");
case LOW -> luck > config.getChance("low_rarity_chance"); case LOW -> luck > plugin.getConfig().getChance("low_rarity_chance");
case NONE -> true; case NONE -> true;
}; };
} }
@ -34,4 +36,8 @@ public abstract class AbstractListener implements Listener {
LOW, LOW,
NONE NONE
} }
public Audience asAudience(final Player player) {
return player;
}
} }

View File

@ -1,28 +1,35 @@
package io.github.simplex.luck.listener; package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import io.github.simplex.luck.util.SneakyWorker; import io.github.simplex.luck.util.SneakyWorker;
import java.util.List;
import org.bukkit.entity.Item; import org.bukkit.entity.Item;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.block.BlockDropItemEvent; import org.bukkit.event.block.BlockDropItemEvent;
import java.util.List; public final class BlockDrops extends AbstractListener
{
public final class BlockDrops extends AbstractListener { public BlockDrops(FeelingLucky plugin)
public BlockDrops(FeelingLucky plugin) { {
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void extraBlockDrops(BlockDropItemEvent event) { public void extraBlockDrops(BlockDropItemEvent event)
{
Player player = event.getPlayer(); Player player = event.getPlayer();
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
List<Item> items = event.getItems(); List<Item> items = event.getItems();
if (luck.quickRNG(luck.getPercentage()) && doesQualify("block_drops", luck.getPercentage())) { if (luck.quickRNG(luck.getValue()) && doesQualify("block_drops", luck.getValue()))
event.getItems().clear(); {
event.getItems().addAll(items.stream().map(SneakyWorker::move).toList()); event.getItems().addAll(items.stream().map(SneakyWorker::move).toList());
} }
if (luck.isVerbose())
asAudience(player).sendMessage(MiniComponent.info("You got lucky and received extra drops!"));
} }
} }

View File

@ -4,6 +4,7 @@ import io.github.simplex.lib.ItemBuilder;
import io.github.simplex.lib.MiniComponent; import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import net.kyori.adventure.audience.Audience;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.data.Ageable; import org.bukkit.block.data.Ageable;
@ -17,11 +18,13 @@ import org.bukkit.inventory.ItemStack;
public final class BonemealFullCrop extends AbstractListener { public final class BonemealFullCrop extends AbstractListener {
public BonemealFullCrop(FeelingLucky plugin) { public BonemealFullCrop(FeelingLucky plugin) {
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void bonemealFullCrop(PlayerInteractEvent event) { public void bonemealFullCrop(PlayerInteractEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
Audience pAud = player;
Action action = event.getAction(); Action action = event.getAction();
ItemStack bonemeal = ItemBuilder.of(Material.BONE_MEAL).build(); ItemStack bonemeal = ItemBuilder.of(Material.BONE_MEAL).build();
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
@ -37,12 +40,17 @@ public final class BonemealFullCrop extends AbstractListener {
if (action.isRightClick() if (action.isRightClick()
&& handItem.isSimilar(bonemeal) && handItem.isSimilar(bonemeal)
&& (data instanceof Ageable crop) && (data instanceof Ageable crop)
&& luck.quickRNG(luck.getPercentage()) && luck.quickRNG(luck.getValue())
&& doesQualify("bonemeal", luck.getPercentage())) { && doesQualify("bonemeal", luck.getValue())) {
crop.setAge(crop.getMaximumAge()); crop.setAge(crop.getMaximumAge());
data.merge(crop); data.merge(crop);
block.setBlockData(data); block.setBlockData(data);
player.sendMessage(MiniComponent.info("You got lucky and your crops grew to maturity."));
if (luck.isVerbose()) {
asAudience(player).sendMessage(MiniComponent.info("Your luck has caused your crop to become ready for" +
" " +
"harvest!"));
}
} }
} }
} }

View File

@ -3,25 +3,35 @@ package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent; import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import net.kyori.adventure.audience.Audience;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
public final class CheatDeath extends AbstractListener { public final class CheatDeath extends AbstractListener
public CheatDeath(FeelingLucky plugin) { {
public CheatDeath(FeelingLucky plugin)
{
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void cheatDeath(PlayerDeathEvent event) { public void cheatDeath(PlayerDeathEvent event)
{
Player player = event.getPlayer(); Player player = event.getPlayer();
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
double absorption = Math.round(Luck.RNG().nextDouble(5.0, 10.0)); double absorption = Math.round(Luck.RNG().nextDouble(5.0, 10.0));
if (luck.quickRNG(luck.getPercentage()) && doesQualify("cheat_death", luck.getPercentage())) { if (luck.quickRNG(luck.getValue()) && doesQualify("cheat_death", luck.getValue()))
{
event.setCancelled(true); event.setCancelled(true);
player.setHealth(1.0); player.setHealth(1.0);
player.setAbsorptionAmount(absorption); player.setAbsorptionAmount(absorption);
player.sendMessage(MiniComponent.of("You got lucky and cheated death!").send());
if (luck.isVerbose())
{
asAudience(player).sendMessage(MiniComponent.info("You got lucky and cheated death!"));
}
} }
} }
} }

View File

@ -1,39 +1,52 @@
package io.github.simplex.luck.listener; package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import java.util.List;
import java.util.Map;
import org.bukkit.enchantments.Enchantment; import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.enchantment.EnchantItemEvent; import org.bukkit.event.enchantment.EnchantItemEvent;
import java.util.List; public final class EnchantmentBoost extends AbstractListener
import java.util.Map; {
public EnchantmentBoost(FeelingLucky plugin)
public final class EnchantmentBoost extends AbstractListener { {
public EnchantmentBoost(FeelingLucky plugin) {
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void enchantItem(EnchantItemEvent event) { public void enchantItem(EnchantItemEvent event)
{
Map<Enchantment, Integer> enchMap = event.getEnchantsToAdd(); Map<Enchantment, Integer> enchMap = event.getEnchantsToAdd();
List<Enchantment> enchList = enchMap.keySet().stream().toList(); List<Enchantment> enchList = enchMap.keySet().stream().toList();
Player player = event.getEnchanter(); Player player = event.getEnchanter();
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
if (luck.quickRNG(luck.getPercentage()) && doesQualify("enchanting", luck.getPercentage())) { if (luck.quickRNG(luck.getValue()) && doesQualify("enchanting", luck.getValue()))
{
Enchantment particular = enchList.get(Luck.RNG().nextInt(enchList.size())); Enchantment particular = enchList.get(Luck.RNG().nextInt(enchList.size()));
int rng = Luck.RNG().nextInt(1, 5); int rng = Luck.RNG().nextInt(1, 5);
if ((enchMap.get(particular) + rng) > particular.getMaxLevel()) { if ((enchMap.get(particular) + rng) > particular.getMaxLevel())
{
enchMap.replace(particular, particular.getMaxLevel()); enchMap.replace(particular, particular.getMaxLevel());
} }
enchMap.replace(particular, enchMap.get(particular) + rng); enchMap.replace(particular, enchMap.get(particular) + rng);
if (luck.isVerbose())
{
asAudience(player).sendMessage(
MiniComponent.info("Your luck has given you an extra random enchantment."));
}
} }
} }
public FeelingLucky plugin() { public FeelingLucky plugin()
{
return plugin; return plugin;
} }
} }

View File

@ -1,27 +1,38 @@
package io.github.simplex.luck.listener; package io.github.simplex.luck.listener;
import com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent; import com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
public final class ExpBoost extends AbstractListener { public final class ExpBoost extends AbstractListener
public ExpBoost(FeelingLucky plugin) { {
public ExpBoost(FeelingLucky plugin)
{
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void boostExperienceGain(PlayerPickupExperienceEvent event) { public void boostExperienceGain(PlayerPickupExperienceEvent event)
{
ExperienceOrb orb = event.getExperienceOrb(); ExperienceOrb orb = event.getExperienceOrb();
int n = orb.getExperience(); int n = orb.getExperience();
int math = (5 * n ^ 2) / (2 * n + 4); int math = (5 * n ^ 2) / (2 * n + 4);
int rounded = Math.round(math); int rounded = Math.round(math);
Player player = event.getPlayer(); Player player = event.getPlayer();
Luck luck = plugin.getHandler().getLuckContainer(player); Luck luck = plugin.getHandler().getLuckContainer(player);
if (luck.quickRNG(luck.getPercentage()) && doesQualify("experience", luck.getPercentage())) { if (luck.quickRNG(luck.getValue()) && doesQualify("experience", luck.getValue()))
{
orb.setExperience(rounded); orb.setExperience(rounded);
if (luck.isVerbose())
{
asAudience(player).sendMessage(
MiniComponent.info("Your luck has given you extra experience!"));
}
} }
} }
} }

View File

@ -0,0 +1,38 @@
package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
public class GiveDamage extends AbstractListener
{
public GiveDamage(FeelingLucky plugin)
{
super(plugin);
register(this);
}
@EventHandler
public void playerAttack(EntityDamageByEntityEvent e)
{
if ((e.getDamager() instanceof Player player)
&& (e.getEntity() instanceof LivingEntity))
{
double nextDmg = e.getDamage() + Luck.RNG().nextDouble(1.0, 5.0);
Luck luck = plugin.getHandler().getLuckContainer(player);
if (luck.quickRNG(luck.getValue()) && doesQualify("give_damage", luck.getValue()))
{
e.setDamage(nextDmg);
if (luck.isVerbose())
{
asAudience(player).sendMessage(
MiniComponent.info("Your luck gave you a critical hit!"));
}
}
}
}
}

View File

@ -0,0 +1,49 @@
package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Monster;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerToggleSneakEvent;
public class HideCheck extends AbstractListener
{
public HideCheck(FeelingLucky plugin)
{
super(plugin);
register(this);
}
@EventHandler
public void checkForSneak(PlayerToggleSneakEvent event)
{
Player player = event.getPlayer();
if (player.isSneaking())
return;
Luck luck = plugin.getHandler().getLuckContainer(player);
if (luck.quickRNG(luck.getValue()) && doesQualify("hide_check", luck.getValue()))
{
player.getNearbyEntities(25, 25, 25)
.stream()
.filter(e -> e instanceof Monster)
.map(e -> (Monster) e)
.forEach(m ->
{
final LivingEntity target = m.getTarget();
if (target != null && target.getUniqueId().equals(player.getUniqueId()))
{
m.setTarget(null);
}
});
if (luck.isVerbose())
{
asAudience(player).sendMessage(MiniComponent.info("Your luck has hidden you from sight."));
}
}
}
}

View File

@ -11,69 +11,92 @@ import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
public class IllOmen extends AbstractListener { public class IllOmen extends AbstractListener
public IllOmen(FeelingLucky plugin) { {
public IllOmen(FeelingLucky plugin)
{
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void reconnectCheck(PlayerJoinEvent event) { public void reconnectCheck(PlayerJoinEvent event)
{
Player player = event.getPlayer(); Player player = event.getPlayer();
PotionEffectType type = PotionEffectType.BAD_OMEN; PotionEffectType type = PotionEffectType.BAD_OMEN;
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
if (player.hasPotionEffect(type)) { if (player.hasPotionEffect(type))
{
luck.cache(); luck.cache();
double maths = luck.getValue() - (luck.getValue() * 0.25); double maths = luck.getValue() - (luck.getValue() * 0.25);
luck.setValue(maths); luck.setValue(maths);
player.sendMessage(MiniComponent.info("A -25% debuff has been applied to your luck from the Bad Omen status effect.")); asAudience(player).sendMessage(MiniComponent.info("A -25% debuff has been applied to your luck from the " +
} else if (luck.cached(player) && !player.hasPotionEffect(type)) { "Bad Omen " +
"status effect."));
}
else if (luck.cached(player) && !player.hasPotionEffect(type))
{
luck.restore(); luck.restore();
player.sendMessage("The -25% debuff to your luck has been removed."); asAudience(player).sendMessage(MiniComponent.info("The -25% debuff to your luck has been removed."));
} }
} }
@EventHandler @EventHandler
public void effectApplyCheck(EntityPotionEffectEvent event) { public void effectApplyCheck(EntityPotionEffectEvent event)
{
EntityPotionEffectEvent.Cause cause = EntityPotionEffectEvent.Cause.PATROL_CAPTAIN; EntityPotionEffectEvent.Cause cause = EntityPotionEffectEvent.Cause.PATROL_CAPTAIN;
EntityPotionEffectEvent.Action added = EntityPotionEffectEvent.Action.ADDED; EntityPotionEffectEvent.Action added = EntityPotionEffectEvent.Action.ADDED;
EntityPotionEffectEvent.Action changed = EntityPotionEffectEvent.Action.CHANGED; EntityPotionEffectEvent.Action changed = EntityPotionEffectEvent.Action.CHANGED;
if (event.getCause().equals(cause) && (event.getAction().equals(added) || event.getAction().equals(changed))) { if (event.getCause().equals(cause) && (event.getAction().equals(added) || event.getAction().equals(changed)))
if (event.getEntity() instanceof Player player) { {
if (event.getEntity() instanceof Player player)
{
Luck luck = plugin.getHandler().getLuckContainer(player); Luck luck = plugin.getHandler().getLuckContainer(player);
luck.cache(); luck.cache();
double maths = luck.getValue() - (luck.getValue() * 0.25); double maths = luck.getValue() - (luck.getValue() * 0.25);
luck.setValue(maths); luck.setValue(maths);
player.sendMessage(MiniComponent.warn("A -25% debuff has been applied to your luck from the Bad Omen status effect.")); asAudience(player).sendMessage(
MiniComponent.warn("A -25% debuff has been applied to your luck from the Bad Omen status effect."));
} }
} }
} }
@EventHandler @EventHandler
public void effectRemoveCheck(EntityPotionEffectEvent event) { public void effectRemoveCheck(EntityPotionEffectEvent event)
{
PotionEffect old = event.getOldEffect(); PotionEffect old = event.getOldEffect();
EntityPotionEffectEvent.Action cleared = EntityPotionEffectEvent.Action.CLEARED; EntityPotionEffectEvent.Action cleared = EntityPotionEffectEvent.Action.CLEARED;
EntityPotionEffectEvent.Action removed = EntityPotionEffectEvent.Action.REMOVED; EntityPotionEffectEvent.Action removed = EntityPotionEffectEvent.Action.REMOVED;
if (old == null) return; if (old == null)
return;
if (old.getType().equals(PotionEffectType.BAD_OMEN) && (event.getAction().equals(cleared) || event.getAction().equals(removed))) { if (old.getType().equals(PotionEffectType.BAD_OMEN) && (event.getAction().equals(cleared) || event.getAction()
if ((event.getEntity() instanceof Player player)) { .equals(
removed)))
{
if ((event.getEntity() instanceof Player player))
{
Luck luck = plugin.getHandler().getLuckContainer(player); Luck luck = plugin.getHandler().getLuckContainer(player);
if (luck.cached(player)) { if (luck.cached(player))
{
luck.restore(); luck.restore();
player.sendMessage("The -25% debuff to your luck has been removed."); asAudience(player).sendMessage(MiniComponent.info("The -25% debuff to your luck has been removed."));
} }
} }
} }
} }
@EventHandler @EventHandler
public void disconnectCheck(PlayerQuitEvent event) { public void disconnectCheck(PlayerQuitEvent event)
if (event.getPlayer().hasPotionEffect(PotionEffectType.BAD_OMEN)) { {
if (event.getPlayer().hasPotionEffect(PotionEffectType.BAD_OMEN))
{
Luck luck = plugin.getHandler().getLuckContainer(event.getPlayer()); Luck luck = plugin.getHandler().getLuckContainer(event.getPlayer());
if (luck.cached(event.getPlayer())) { if (luck.cached(event.getPlayer()))
{
luck.restore(); luck.restore();
} }
} }

View File

@ -1,7 +1,11 @@
package io.github.simplex.luck.listener; package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.Item; import org.bukkit.entity.Item;
import org.bukkit.entity.LivingEntity; import org.bukkit.entity.LivingEntity;
@ -12,29 +16,32 @@ import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityDropItemEvent; import org.bukkit.event.entity.EntityDropItemEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import java.util.HashMap; public class ItemDrops extends AbstractListener
import java.util.Map; {
import java.util.UUID;
public class ItemDrops extends AbstractListener {
private final Map<UUID, Player> entityPlayerMap = new HashMap<>(); private final Map<UUID, Player> entityPlayerMap = new HashMap<>();
private boolean canAffect = false; private boolean canAffect = false;
public ItemDrops(FeelingLucky plugin) { public ItemDrops(FeelingLucky plugin)
{
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void checkForPreItemDrop(EntityDamageByEntityEvent event) { public void checkForPreItemDrop(EntityDamageByEntityEvent event)
if (!(event.getEntity() instanceof LivingEntity entity)) { {
if (!(event.getEntity() instanceof LivingEntity entity))
{
return; return;
} }
if (!(event.getDamager() instanceof Player player)) { if (!(event.getDamager() instanceof Player player))
{
return; return;
} }
if (!(entity.getHealth() <= 0.0)) { if (entity.getHealth() > 0.0)
{
return; return;
} }
@ -42,8 +49,10 @@ public class ItemDrops extends AbstractListener {
} }
@EventHandler @EventHandler
public void checkForDroppedItems(EntityDeathEvent event) { public void checkForDroppedItems(EntityDeathEvent event)
if (event.getEntity() instanceof Player) { {
if (event.getEntity() instanceof Player)
{
canAffect = false; canAffect = false;
return; return;
} }
@ -52,23 +61,32 @@ public class ItemDrops extends AbstractListener {
} }
@EventHandler @EventHandler
public void itemDrops(EntityDropItemEvent event) { public void itemDrops(EntityDropItemEvent event)
{
Entity entity = event.getEntity(); Entity entity = event.getEntity();
if (entityPlayerMap.get(entity.getUniqueId()) == null) return; if (entityPlayerMap.get(entity.getUniqueId()) == null)
return;
if (!canAffect) return; if (!canAffect)
return;
Player player = entityPlayerMap.get(entity.getUniqueId()); Player player = entityPlayerMap.get(entity.getUniqueId());
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
Item item = event.getItemDrop(); Item item = event.getItemDrop();
ItemStack stack = item.getItemStack(); ItemStack stack = item.getItemStack();
int amount = stack.getAmount(); int amount = stack.getAmount();
if (luck.quickRNG(luck.getPercentage()) && doesQualify("item_drops", luck.getPercentage())) { if (luck.quickRNG(luck.getValue()) && doesQualify("item_drops", luck.getValue()))
{
int rng = Luck.RNG().nextInt(2, 5); int rng = Luck.RNG().nextInt(2, 5);
amount += rng; amount += rng;
stack.setAmount(amount); stack.setAmount(amount);
event.getItemDrop().setItemStack(stack); event.getItemDrop().setItemStack(stack);
if (luck.isVerbose())
{
asAudience(player).sendMessage(MiniComponent.info("Your luck earned you some extra loot!"));
}
} }
} }
} }

View File

@ -0,0 +1,31 @@
package io.github.simplex.luck.listener;
import com.destroystokyo.paper.event.player.PlayerJumpEvent;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.util.Vector;
public class JumpBoost extends AbstractListener {
public JumpBoost(FeelingLucky plugin) {
super(plugin);
register(this);
}
@EventHandler
public void detectJumping(PlayerJumpEvent event) {
Player player = event.getPlayer(); // Player is never null; they're in game and jumping.
Luck luck = plugin.getHandler().getLuckContainer(player);
Vector velocity = player.getVelocity().clone();
if (luck.quickRNG(luck.getValue()) && doesQualify("jump_boost", luck.getValue())) {
player.setVelocity(velocity.multiply(2.5));
if (luck.isVerbose()) {
asAudience(player).sendMessage(MiniComponent.info("Your luck gave you an extra boost to your jump!"));
}
}
}
}

View File

@ -0,0 +1,60 @@
package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.BlockBreakEvent;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.ApiStatus;
/**
* This class is currently unstable.
*/
@Deprecated
@ApiStatus.Experimental
public class OreVein extends AbstractListener {
public OreVein(FeelingLucky plugin) {
super(plugin);
register(this);
}
@EventHandler
public void playerMine(BlockBreakEvent event) {
Player player = event.getPlayer();
Luck luck = plugin.getHandler().getLuckContainer(player);
if (luck.quickRNG(luck.getValue()) && doesQualify("ore_vein", luck.getValue()) && event.getBlock().isValidTool(player.getInventory().getItemInMainHand())) {
getOresInArea(event.getBlock()).forEach(Block::breakNaturally);
player.sendMessage(MiniComponent.info("Your luck has let you mine all the blocks with one swing."));
}
}
public List<Block> getOresInArea(Block block) {
Stream.Builder<Block> streamBuilder = Stream.builder();
Location start = block.getLocation();
World world = block.getWorld();
List<Tag<Material>> materialList = List.of(Tag.COAL_ORES, Tag.COPPER_ORES, Tag.DIAMOND_ORES, Tag.GOLD_ORES, Tag.IRON_ORES, Tag.EMERALD_ORES, Tag.LAPIS_ORES, Tag.REDSTONE_ORES);
for (int x = start.getBlockX() - 15; x <= start.getBlockX() + 15; x++) {
for (int y = start.getBlockY() - 15; y <= start.getBlockY() + 15; y++) {
for (int z = start.getBlockZ() - 15; z <= start.getBlockZ() + 15; z++) {
Location location = new Location(world, x, y, z);
Material blockType = location.getBlock().getType();
if (materialList.stream().anyMatch(o -> o.isTagged(blockType))) {
streamBuilder.add(location.getBlock());
}
}
}
}
return streamBuilder.build().filter(b -> b.getType().equals(block.getType())).toList();
}
}

View File

@ -25,33 +25,36 @@ public final class PlayerListener extends AbstractListener {
public PlayerListener(FeelingLucky plugin) { public PlayerListener(FeelingLucky plugin) {
super(plugin); super(plugin);
this.timer = new CooldownTimer(); this.timer = new CooldownTimer();
register(this);
} }
@EventHandler @EventHandler
public void rabbitFoot(PlayerInteractEvent event) { public void rabbitFoot(PlayerInteractEvent event) {
Action action = event.getAction(); Action action = event.getAction();
ItemStack foot = new ItemStack(Material.RABBIT_FOOT); ItemStack foot = new ItemStack(Material.RABBIT_FOOT);
SpecialFootItem special = new SpecialFootItem(); SpecialFootItem special = plugin.getFoot();
Player player = event.getPlayer(); Player player = event.getPlayer();
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
if (timer.onCooldown(player)) { if (timer.onCooldown(player)) {
player.sendMessage(MiniComponent.err("That feature can only be used once every 30 seconds.")); asAudience(player).sendMessage(MiniComponent.err("That feature can only be used once every 30 seconds."));
player.sendMessage(MiniComponent.info("You have " + timer.remaining(player) + " seconds remaining.")); asAudience(player).sendMessage(MiniComponent.info("You have " + timer.remaining(player) + " seconds " +
"remaining."));
return; return;
} }
if (action.isRightClick() && player.getInventory().getItemInMainHand().isSimilar(foot)) { if (action.isRightClick() && player.getInventory().getItemInMainHand().getType().equals(foot.getType())) {
if (foot.getItemMeta().equals(special.meta()) || foot.equals(special.get())) { if (foot.getItemMeta().equals(special.meta()) || foot.equals(special.get())) {
luck.setMultiplier(luck.multiplier() + 1); luck.setMultiplier(luck.multiplier() + 0.1);
player.sendMessage(MiniComponent.info("Your luck multiplier has increased by 1!")); asAudience(player).sendMessage(MiniComponent.info("Your luck multiplier has increased by 0.1!"));
} }
double rng = Luck.RNG().nextDouble(2.0, 5.0); double rng = Luck.RNG().nextDouble(2.0, 5.0);
rng = Math.round(rng);
player.getInventory().remove(player.getInventory().getItemInMainHand()); player.getInventory().remove(player.getInventory().getItemInMainHand());
luck.addTo(rng); luck.addTo(rng);
plugin.getHandler().updatePlayer(player, luck); plugin.getHandler().updatePlayer(player, luck);
timer.setCooldown(player.getUniqueId(), System.currentTimeMillis()); timer.setCooldown(player.getUniqueId(), System.currentTimeMillis());
player.sendMessage(MiniComponent.info("Your luck has been increased by " + rng + " points.")); asAudience(player).sendMessage(MiniComponent.info("Your luck has been increased by " + rng + " points."));
} }
} }
@ -74,28 +77,8 @@ public final class PlayerListener extends AbstractListener {
if (luck.quickRNG(33.0)) { if (luck.quickRNG(33.0)) {
luck.takeFrom(5.0); luck.takeFrom(5.0);
plugin.getHandler().updatePlayer(player, luck); plugin.getHandler().updatePlayer(player, luck);
player.sendMessage(MiniComponent.warn("Your luck has been decreased by 5 points!")); asAudience(player).sendMessage(MiniComponent.warn("Your luck has been decreased by 5 points!"));
} }
} }
} }
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (PlayerListener) obj;
return Objects.equals(this.plugin, that.plugin);
}
@Override
public int hashCode() {
return Objects.hash(plugin);
}
@Override
public String toString() {
return "PlayerListener[" +
"plugin=" + plugin + ']';
}
} }

View File

@ -4,21 +4,24 @@ import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import io.github.simplex.luck.util.ListBox; import io.github.simplex.luck.util.ListBox;
import java.util.List;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import java.util.List; public class RandomEffect extends AbstractListener
{
public class RandomEffect extends AbstractListener { public RandomEffect(FeelingLucky plugin)
public RandomEffect(FeelingLucky plugin) { {
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void giveRandomEffect(PlayerRespawnEvent respawn) { public void giveRandomEffect(PlayerRespawnEvent respawn)
{
Player player = respawn.getPlayer(); Player player = respawn.getPlayer();
Luck luck = plugin.getHandler().getLuckContainer(player); Luck luck = plugin.getHandler().getLuckContainer(player);
@ -26,14 +29,21 @@ public class RandomEffect extends AbstractListener {
int size = effectList.size(); int size = effectList.size();
PotionEffect random = effectList.get(Luck.RNG().nextInt(size - 1)); PotionEffect random = effectList.get(Luck.RNG().nextInt(size - 1));
if (luck.quickRNG(luck.getPercentage()) && doesQualify("random_effect", luck.getValue())) { if (luck.quickRNG(luck.getValue()) && doesQualify("random_effect", luck.getValue()))
{
player.addPotionEffect(random); player.addPotionEffect(random);
player.sendMessage(MiniComponent.info("Thanks to luck, a random positive potion effect has been applied to you.")); if (luck.isVerbose())
{
asAudience(player).sendMessage(
MiniComponent.info("Thanks to luck, a random positive potion effect has " +
"been applied to you."));
}
} }
} }
@EventHandler @EventHandler
public void giveRandomEffect(PlayerTeleportEvent tp) { public void giveRandomEffect(PlayerTeleportEvent tp)
{
Player player = tp.getPlayer(); Player player = tp.getPlayer();
Luck luck = plugin.getHandler().getLuckContainer(player); Luck luck = plugin.getHandler().getLuckContainer(player);
@ -41,9 +51,14 @@ public class RandomEffect extends AbstractListener {
int size = effectList.size(); int size = effectList.size();
PotionEffect random = effectList.get(Luck.RNG().nextInt(size - 1)); PotionEffect random = effectList.get(Luck.RNG().nextInt(size - 1));
if (luck.quickRNG(luck.getPercentage()) && doesQualify("random_effect", luck.getValue())) { if (luck.quickRNG(luck.getValue()) && doesQualify("random_effect", luck.getValue()))
{
player.addPotionEffect(random); player.addPotionEffect(random);
player.sendMessage(MiniComponent.info("Thanks to luck, a random positive potion effect has been applied to you.")); if (luck.isVerbose())
{
asAudience(player).sendMessage(
MiniComponent.info("Thanks to luck, a random positive potion effect has been applied to you."));
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
package io.github.simplex.luck.listener; package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.lib.PotionEffectBuilder; import io.github.simplex.lib.PotionEffectBuilder;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
@ -10,26 +11,44 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
public class RestoreHunger extends AbstractListener { public class RestoreHunger extends AbstractListener
public RestoreHunger(FeelingLucky plugin) { {
public RestoreHunger(FeelingLucky plugin)
{
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void restoreHunger(PlayerItemConsumeEvent event) { public void restoreHunger(PlayerItemConsumeEvent event)
{
ItemStack item = event.getItem(); ItemStack item = event.getItem();
Luck luck = getHandler().getLuckContainer(event.getPlayer()); Luck luck = getHandler().getLuckContainer(event.getPlayer());
PotionEffect effect = PotionEffectBuilder.newEffect().type(PotionEffectType.SATURATION).amplifier(2).duration(10).particles(false).create(); PotionEffect effect = PotionEffectBuilder.newEffect()
if (luck.notDefault()) { .type(PotionEffectType.SATURATION)
double percentage = luck.getPercentage(); .amplifier(2)
ListBox.foods.forEach(food -> { .duration(10)
if (item.isSimilar(food)) { .particles(false)
if (luck.quickRNG(percentage) && doesQualify("restore_hunger", percentage)) { .create();
if (luck.notDefault())
{
double percentage = luck.getValue();
ListBox.foods.forEach(food ->
{
if (item.isSimilar(food) && (luck.quickRNG(percentage) && doesQualify(
"restore_hunger", percentage)))
{
event.getPlayer().setExhaustion(event.getPlayer().getExhaustion() + 2); event.getPlayer().setExhaustion(event.getPlayer().getExhaustion() + 2);
event.getPlayer().addPotionEffect(effect); event.getPlayer().addPotionEffect(effect);
}
} }
}); });
if (luck.isVerbose())
{
asAudience(event.getPlayer())
.sendMessage(MiniComponent.info("Your luck has restored your hunger a little more."));
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
package io.github.simplex.luck.listener; package io.github.simplex.luck.listener;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.lib.PotionEffectBuilder; import io.github.simplex.lib.PotionEffectBuilder;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
@ -13,6 +14,7 @@ import org.bukkit.event.entity.EntityDamageEvent;
public class TakeDamage extends AbstractListener { public class TakeDamage extends AbstractListener {
public TakeDamage(FeelingLucky plugin) { public TakeDamage(FeelingLucky plugin) {
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
@ -23,9 +25,8 @@ public class TakeDamage extends AbstractListener {
} }
Player player = (Player) event.getEntity(); Player player = (Player) event.getEntity();
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
if (ListBox.acceptedCauses.contains(event.getCause())) { if (ListBox.acceptedCauses.contains(event.getCause()) && (luck.notDefault())) {
if (luck.notDefault()) { double percentage = luck.getValue();
double percentage = luck.getPercentage();
/* /*
* If a player's luck stat is a negative number, or they are "marked", * If a player's luck stat is a negative number, or they are "marked",
@ -36,7 +37,7 @@ public class TakeDamage extends AbstractListener {
if (luck.quickRNG(percentage)) { if (luck.quickRNG(percentage)) {
event.setCancelled(true); event.setCancelled(true);
player.damage(event.getDamage() * 2); player.damage(event.getDamage() * 2);
player.sendMessage(Component.empty().content("You were unlucky and took double damage.")); asAudience(player).sendMessage(MiniComponent.warn("You were unlucky and took double damage!"));
} }
return; return;
} }
@ -44,14 +45,12 @@ public class TakeDamage extends AbstractListener {
if (luck.quickRNG(percentage) && doesQualify("take_damage", percentage)) { if (luck.quickRNG(percentage) && doesQualify("take_damage", percentage)) {
event.setCancelled(true); event.setCancelled(true);
player.damage(event.getDamage() / 2); player.damage(event.getDamage() / 2);
player.sendMessage(Component.empty().content("You got lucky and took less damage."));
}
}
} }
if (ListBox.sideCauses.contains(event.getCause())) { }
if (luck.notDefault()) {
double percentage = luck.getPercentage(); if (ListBox.sideCauses.contains(event.getCause()) && (luck.notDefault())) {
double percentage = luck.getValue();
/* /*
* If a player's luck stat is a negative number, or they are "marked", * If a player's luck stat is a negative number, or they are "marked",
@ -70,9 +69,10 @@ public class TakeDamage extends AbstractListener {
event.setCancelled(true); event.setCancelled(true);
player.getActivePotionEffects().removeIf(p -> ListBox.potionEffects.contains(p.getType())); player.getActivePotionEffects().removeIf(p -> ListBox.potionEffects.contains(p.getType()));
player.setFireTicks(0); player.setFireTicks(0);
player.sendMessage(Component.empty().content("You got lucky and your afflictions were cured.")); asAudience(player).sendMessage(MiniComponent.info("You got lucky and your afflictions were cured" +
} "."));
} }
} }
} }
} }

View File

@ -11,29 +11,41 @@ import org.bukkit.inventory.CraftingInventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
public class UnbreakableTool extends AbstractListener { public class UnbreakableTool extends AbstractListener
public UnbreakableTool(FeelingLucky plugin) { {
public UnbreakableTool(FeelingLucky plugin)
{
super(plugin); super(plugin);
register(this);
} }
@EventHandler @EventHandler
public void unbreakableTool(CraftItemEvent event) { public void unbreakableTool(CraftItemEvent event)
{
CraftingInventory inventory = event.getInventory(); CraftingInventory inventory = event.getInventory();
ItemStack stack = inventory.getResult(); ItemStack stack = inventory.getResult();
if (stack == null) return; if (stack == null)
return;
ItemMeta meta = stack.getItemMeta(); ItemMeta meta = stack.getItemMeta();
if (ItemBuilder.isTool(stack.getType())) { if (ItemBuilder.isTool(stack.getType()) && (event.getWhoClicked() instanceof Player player))
if (event.getWhoClicked() instanceof Player player) { {
Luck luck = getHandler().getLuckContainer(player); Luck luck = getHandler().getLuckContainer(player);
if (luck.quickRNG(luck.getPercentage()) && doesQualify("unbreakable", luck.getPercentage())) { if (luck.quickRNG(luck.getValue()) && doesQualify("unbreakable", luck.getValue()))
{
meta.setUnbreakable(true); meta.setUnbreakable(true);
stack.setItemMeta(meta); stack.setItemMeta(meta);
inventory.setResult(stack); inventory.setResult(stack);
player.sendMessage(MiniComponent.info("By the grace of Luck you have crafted an unbreakable tool!"));
} if (luck.isVerbose())
} {
asAudience(player)
.sendMessage(
MiniComponent.info("By the grace of Luck you have crafted an unbreakable tool!"));
}
}
} }
} }
} }

View File

@ -15,12 +15,14 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
public class VillagerInventory extends AbstractListener { public class VillagerInventory extends AbstractListener {
private final SpecialFootItem foot = new SpecialFootItem(); private final MerchantRecipe recipe;
private final MerchantRecipe recipe = new MerchantRecipe(foot.get(), 0, 2, true);
public VillagerInventory(FeelingLucky plugin) { public VillagerInventory(FeelingLucky plugin) {
super(plugin); super(plugin);
SpecialFootItem foot = plugin.getFoot();
this.recipe = new MerchantRecipe(foot.get(), 0, 2, true);
recipe.setIngredients(Arrays.asList( recipe.setIngredients(Arrays.asList(
ItemBuilder.of(Material.EMERALD).build(), ItemBuilder.of(Material.EMERALD).build(),
ItemBuilder.of(Material.RABBIT_HIDE).build() ItemBuilder.of(Material.RABBIT_HIDE).build()
@ -29,6 +31,8 @@ public class VillagerInventory extends AbstractListener {
recipe.setPriceMultiplier(1.25F); recipe.setPriceMultiplier(1.25F);
recipe.setVillagerExperience(25); recipe.setVillagerExperience(25);
recipe.setSpecialPrice(4); recipe.setSpecialPrice(4);
register(this);
} }
@EventHandler @EventHandler
@ -42,7 +46,7 @@ public class VillagerInventory extends AbstractListener {
Luck luck = plugin.getHandler().getLuckContainer(event.getPlayer()); Luck luck = plugin.getHandler().getLuckContainer(event.getPlayer());
if (luck == null) return; if (luck == null) return;
if (luck.quickRNG(luck.getPercentage())) { if (luck.quickRNG(luck.getValue())) {
recipeList.add(recipe); recipeList.add(recipe);
vil.setRecipes(recipeList); vil.setRecipes(recipeList);
} }

View File

@ -3,12 +3,17 @@ package io.github.simplex.luck.player;
import io.github.simplex.api.LuckContainer; import io.github.simplex.api.LuckContainer;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.*; import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@SuppressWarnings("all") @SuppressWarnings("all")
public class Luck implements LuckContainer { public class Luck implements LuckContainer {
@ -20,6 +25,7 @@ public class Luck implements LuckContainer {
private double BASE_VALUE; private double BASE_VALUE;
private double multiplier; private double multiplier;
private double tempSave; private double tempSave;
private boolean verbose;
public Luck(FeelingLucky plugin, Player player) { public Luck(FeelingLucky plugin, Player player) {
this(plugin, player, 1.0); this(plugin, player, 1.0);
@ -35,15 +41,21 @@ public class Luck implements LuckContainer {
event = new PlayerLuckChangeEvent(this); event = new PlayerLuckChangeEvent(this);
} }
/**
* This creates a new instance of a pseudorandom number generator based off entropy provided by the operating system.
* This will allow for a much purer randomization, due to entropy being different for each call.
*
* @return A new instance of SecureRandom. Each time this method is called a new instance is created to provide maximum variation with entropic calculations.
*/
@Contract(pure = true, @Contract(pure = true,
value = "-> new") value = "-> new")
public static @NotNull SplittableRandom RNG() { public static @NotNull SecureRandom RNG() {
return new SplittableRandom(); return new SecureRandom(SecureRandom.getSeed(20));
} }
public static boolean quickRNGnoMultiplier(double percentage) { public static boolean quickRNGnoMultiplier(double value) {
double rng; double rng;
if (percentage >= 100.0) { if (value >= 1024.0) {
rng = 1024.0; // 100% chance to trigger, obviously; rng = 1024.0; // 100% chance to trigger, obviously;
} else { } else {
rng = RNG().nextDouble(0.0, 1024.0); rng = RNG().nextDouble(0.0, 1024.0);
@ -51,7 +63,11 @@ public class Luck implements LuckContainer {
double actual = Math.round((rng / 1024.0) * 100); double actual = Math.round((rng / 1024.0) * 100);
return (percentage >= actual); return (value >= actual);
}
public boolean playerHasLuckPE() {
return player.hasPotionEffect(PotionEffectType.LUCK);
} }
public FeelingLucky getPlugin() { public FeelingLucky getPlugin() {
@ -71,23 +87,23 @@ public class Luck implements LuckContainer {
} }
@Override @Override
public Attribute asAttribute() { public void setVerbose(final boolean verbose) {
return Attribute.GENERIC_LUCK; this.verbose = verbose;
} }
@Override @Override
public double getNumber() { public boolean isVerbose() {
return associatedPlayer().getAttribute(asAttribute()).getValue(); return verbose;
} }
@Override @Override
public boolean isMatch(double number) { public boolean isMatch(double number) {
return getNumber() == number; return getValue() == number;
} }
@Override @Override
public boolean isClose(double number, int range) { public boolean isClose(double number, int range) {
return ((getNumber() - range <= number) && (number <= getNumber() + range)); return ((getValue() - range <= number) && (number <= getValue() + range));
} }
@Override @Override
@ -100,21 +116,33 @@ public class Luck implements LuckContainer {
return player; return player;
} }
public boolean quickRNG(double percentage) { /**
* Quickly calculate whether or not the player has enough luck to trigger the condition.
*
* @param value The players luck value.
* @return True if the player meets the criteria, false if they do not.
*/
public boolean quickRNG(double value) {
double rng; double rng;
if (percentage >= 100.0) { if (value >= 1024.0) {
rng = 1024.0; // 100% chance to trigger, obviously; rng = 1024.0; // 100% chance to trigger, obviously;
} else { } else {
rng = RNG().nextDouble(0.0, 1024.0); rng = RNG().nextDouble(0.0, 1024.0);
} }
AtomicReference<Double> multiplier = new AtomicReference<>(multiplier());
double actual = Math.round((rng / 1024) * 100); double actual = Math.round((rng / 1024) * 100);
double newVal = Math.round((value / 1024) * 100);
if (multiplier() > 1.0) { if (playerHasLuckPE()) {
return ((percentage * multiplier()) >= actual); player.getActivePotionEffects()
.stream()
.filter(p -> p.getType().equals(PotionEffectType.LUCK))
.findFirst()
.ifPresent(p -> multiplier.updateAndGet(v -> (v + p.getAmplifier())));
} }
return (percentage >= actual); return ((newVal * multiplier.get()) >= actual);
} }
public void reset() { public void reset() {
@ -128,7 +156,6 @@ public class Luck implements LuckContainer {
public void setValue(double value) { public void setValue(double value) {
BASE_VALUE = value; BASE_VALUE = value;
player.getAttribute(Attribute.GENERIC_LUCK).setBaseValue(value);
plugin.getConfigMap().get(associatedPlayer().getUniqueId()).setLuck(value); plugin.getConfigMap().get(associatedPlayer().getUniqueId()).setLuck(value);
Bukkit.getPluginManager().callEvent(event); Bukkit.getPluginManager().callEvent(event);
} }
@ -162,7 +189,7 @@ public class Luck implements LuckContainer {
} }
public double getDefaultValue() { public double getDefaultValue() {
return player.getAttribute(Attribute.GENERIC_LUCK).getDefaultValue(); return 0;
} }
public void addTo(double value) { public void addTo(double value) {
@ -181,10 +208,6 @@ public class Luck implements LuckContainer {
setValue(getValue() + value); setValue(getValue() + value);
} }
public double getPercentage() {
return getValue() - getDefaultValue();
}
public boolean notDefault() { public boolean notDefault() {
return getValue() != getDefaultValue(); return getValue() != getDefaultValue();
} }

View File

@ -1,55 +1,38 @@
package io.github.simplex.luck.player; package io.github.simplex.luck.player;
import io.github.simplex.luck.FeelingLucky; import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.util.SneakyWorker; import io.github.simplex.luck.util.Logs;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.attribute.Attribute; import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedWriter; public class PlayerConfig
import java.io.File; {
import java.io.FileWriter;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public class PlayerConfig {
private final File configFile; private final File configFile;
private final OfflinePlayer player; private final OfflinePlayer player;
private volatile YamlConfiguration config; private final FeelingLucky plugin;
private YamlConfiguration config;
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public PlayerConfig(FeelingLucky plugin, Player player) { public PlayerConfig(FeelingLucky plugin, Player player)
{
this.plugin = plugin;
this.player = player; this.player = player;
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs(); final File file = configFile(plugin, player);
File dataFolder = new File(plugin.getDataFolder(), "players");
if (!dataFolder.exists()) dataFolder.mkdirs();
File file = new File(dataFolder, player.getUniqueId() + ".yml");
if (!file.exists()) {
String name = "username: " + player.getName();
String luck = "luck: " + player.getAttribute(Attribute.GENERIC_LUCK).getDefaultValue();
String multiplier = "multiplier: " + 1.0;
SneakyWorker.sneakyTry(() -> {
file.createNewFile();
BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8));
writer.write(name);
writer.newLine();
writer.write(luck);
writer.newLine();
writer.write(multiplier);
writer.close();
});
}
configFile = file; configFile = file;
config = YamlConfiguration.loadConfiguration(configFile); config = YamlConfiguration.loadConfiguration(configFile);
String tempUsername = config.getString("username"); String tempUsername = config.getString("username");
if (tempUsername == null) { if (tempUsername == null)
{
config.set("username", player.getName()); config.set("username", player.getName());
config.set("luck", plugin.getHandler().getLuckContainer(player).getDefaultValue()); config.set("luck", plugin.getHandler().getLuckContainer(player).getDefaultValue());
config.set("multiplier", "1.0"); config.set("multiplier", "1.0");
@ -57,50 +40,141 @@ public class PlayerConfig {
} }
} }
protected PlayerConfig(FeelingLucky plugin, File file) { protected PlayerConfig(FeelingLucky plugin, File file)
{
this.plugin = plugin;
this.configFile = file; this.configFile = file;
this.player = Bukkit.getOfflinePlayer(UUID.fromString(file.getName().split("\\.")[0])); this.player = Bukkit.getOfflinePlayer(UUID.fromString(file.getName().split("\\.")[0]));
config = YamlConfiguration.loadConfiguration(configFile); config = YamlConfiguration.loadConfiguration(configFile);
} }
@Contract("_, _ -> new") @Contract("_, _ -> new")
public static PlayerConfig loadFrom(FeelingLucky plugin, File file) { public static PlayerConfig initFrom(FeelingLucky plugin, File file)
{
return new PlayerConfig(plugin, file); return new PlayerConfig(plugin, file);
} }
public void save() { @NotNull
SneakyWorker.sneakyTry(() -> config.save(configFile)); private File configFile(FeelingLucky plugin, Player player)
{
if (!plugin.getDataFolder().exists())
plugin.getDataFolder().mkdirs();
File dataFolder = new File(plugin.getDataFolder(), "players");
if (!dataFolder.exists())
dataFolder.mkdirs();
File file = new File(dataFolder, player.getUniqueId() + ".yml");
if (!file.exists())
{
try
{
file.createNewFile();
final YamlConfiguration v0 = new YamlConfiguration();
v0.set("username", player.getName());
v0.set("luck", 0);
v0.set("multiplier", 1.0);
v0.set("verbose", true);
v0.save(file);
}
catch (IOException ex)
{
Logs.error(ex);
}
}
return file;
} }
public void load() { public void save()
SneakyWorker.sneakyTry(() -> config = YamlConfiguration.loadConfiguration(configFile)); {
try
{
config.save(configFile);
}
catch (IOException ex)
{
Logs.error(ex);
}
} }
public void reload() { public void load()
{
try
{
config.load(configFile);
}
catch (IOException | InvalidConfigurationException ex)
{
Logs.error(ex);
Logs.warn("Attempting to reinitialize variable... this is dangerous!");
try
{
config = YamlConfiguration.loadConfiguration(configFile);
}
catch (IllegalArgumentException th)
{
Logs.error(th);
}
}
}
public void reload()
{
save(); save();
load(); load();
} }
public OfflinePlayer getPlayer() { public OfflinePlayer getPlayer()
{
return player; return player;
} }
public void setUsername(String name) { public String getUsername() {
config.set("username", name); return config.getString("username");
save();
} }
public void setLuck(double luck) { public double getLuck()
{
return config.getDouble("luck");
}
public void setLuck(double luck)
{
config.set("luck", luck); config.set("luck", luck);
save(); reload();
} }
public void setMultiplier(double multiplier) { public double getMultiplier()
{
return config.getDouble("multiplier");
}
public void setMultiplier(double multiplier)
{
config.set("multiplier", multiplier); config.set("multiplier", multiplier);
save(); reload();
} }
public YamlConfiguration getConfig() { public boolean isVerbose()
{
return config.getBoolean("verbose");
}
public void setVerbose(final boolean verbose)
{
config.set("verbose", verbose);
reload();
}
public void setUsername(String name)
{
config.set("username", name);
reload();
}
public YamlConfiguration getConfig()
{
return config; return config;
} }
} }

View File

@ -38,9 +38,10 @@ public class PlayerHandler implements Listener {
plugin.getConfigMap().put(player.getUniqueId(), playerConfig); plugin.getConfigMap().put(player.getUniqueId(), playerConfig);
} }
String username = playerConfig.getConfig().getString("username"); String username = playerConfig.getUsername();
double luck = playerConfig.getConfig().getDouble("luck"); double luck = playerConfig.getLuck();
double multiplier = playerConfig.getConfig().getDouble("multiplier"); double multiplier = playerConfig.getMultiplier();
boolean verbose = playerConfig.isVerbose();
if (!player.getName().equalsIgnoreCase(username)) { if (!player.getName().equalsIgnoreCase(username)) {
playerConfig.getConfig().set("username", player.getName()); playerConfig.getConfig().set("username", player.getName());
@ -49,6 +50,7 @@ public class PlayerHandler implements Listener {
} }
Luck container = new Luck(plugin, player, multiplier); Luck container = new Luck(plugin, player, multiplier);
container.setVerbose(verbose);
container.setValue(luck); container.setValue(luck);
playerLuckMap.put(player, container); playerLuckMap.put(player, container);

View File

@ -0,0 +1,52 @@
package io.github.simplex.luck.util;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Logs
{
private static final Logger logger = LoggerFactory.getLogger("FeelingLucky");
private Logs()
{
throw new AssertionError();
}
public static void info(String message)
{
logger.info(message);
}
public static void warn(String message)
{
logger.warn(message);
}
public static void warn(String message, Throwable th)
{
logger.warn(message, th);
}
public static void warn(Throwable th)
{
final String msg = ExceptionUtils.getRootCauseMessage(th);
logger.warn(msg);
}
public static void error(String message)
{
logger.error(message);
}
public static void error(String message, Throwable th)
{
logger.error(message, th);
}
public static void error(Throwable th)
{
final String msg = ExceptionUtils.getRootCauseMessage(th);
logger.error(msg, th);
}
}

View File

@ -6,10 +6,7 @@ import io.github.simplex.luck.FeelingLucky;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import io.github.simplex.luck.player.PlayerConfig; import io.github.simplex.luck.player.PlayerConfig;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.Command; import org.bukkit.command.*;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -18,19 +15,23 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
public class LuckCMD extends Command implements TabCompleter { public class LuckCMD extends Command implements TabCompleter, PluginIdentifiableCommand {
private final FeelingLucky plugin; private final FeelingLucky plugin;
public LuckCMD(FeelingLucky plugin) { public LuckCMD(FeelingLucky plugin) {
super("luck", "FeelingLucky main command.", "/<command> <info | set | reset | give | take> [player] [amount]", List.of()); super("luck", "FeelingLucky main command.", "/<command> <info | set | reset | give | take> [player] [amount]", List.of());
this.plugin = plugin; this.plugin = plugin;
setPermission("luck.default"); setPermission("luck.default");
plugin.getServer().getCommandMap().register("luck", "FeelingLucky", this); plugin.getCommandMap().register("luck", "FeelingLucky", this);
plugin.getLogger().info("Successfully registered command: Luck");
} }
@Override @Override
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
if (args.length < 1 || args.length > 3) return false; if (args.length < 1 || args.length > 3) {
sender.sendMessage(this.getUsage());
return false;
}
if (args.length == 3) { if (args.length == 3) {
if ((sender instanceof ConsoleCommandSender) || sender.hasPermission("luck.admin")) { if ((sender instanceof ConsoleCommandSender) || sender.hasPermission("luck.admin")) {
@ -68,21 +69,21 @@ public class LuckCMD extends Command implements TabCompleter {
luck.setValue(amount); luck.setValue(amount);
plugin.getHandler().updatePlayer(player, luck); plugin.getHandler().updatePlayer(player, luck);
config.setLuck(luck.getValue()); config.setLuck(luck.getValue());
sender.sendMessage(MiniComponent.info("Successfully reset " + args[1] + "'s Luck stat.")); sender.sendMessage(MiniComponent.info("Successfully set " + args[1] + "'s Luck stat to " + amount + "."));
return true; return true;
} }
case "give" -> { case "give" -> {
luck.addTo(amount); luck.addTo(amount);
plugin.getHandler().updatePlayer(player, luck); plugin.getHandler().updatePlayer(player, luck);
config.setLuck(luck.getValue()); config.setLuck(luck.getValue());
sender.sendMessage(MiniComponent.info("Successfully reset " + args[1] + "'s Luck stat.")); sender.sendMessage(MiniComponent.info("Successfully gave " + args[1] + " " + amount + " points of luck!"));
return true; return true;
} }
case "take" -> { case "take" -> {
luck.takeFrom(amount); luck.takeFrom(amount);
plugin.getHandler().updatePlayer(player, luck); plugin.getHandler().updatePlayer(player, luck);
config.setLuck(luck.getValue()); config.setLuck(luck.getValue());
sender.sendMessage(MiniComponent.info("Successfully reset " + args[1] + "'s Luck stat.")); sender.sendMessage(MiniComponent.info("Successfully took " + amount + " points of luck from " + args[1]));
return true; return true;
} }
} }
@ -113,6 +114,17 @@ public class LuckCMD extends Command implements TabCompleter {
return true; return true;
} }
if (args[0].equalsIgnoreCase("verbose") && sender instanceof Player player) {
final boolean a1 = Boolean.parseBoolean(args[1]);
Luck luck = plugin.getHandler().getLuckContainer(player);
PlayerConfig config = plugin.getConfigMap().get(player.getUniqueId());
luck.setVerbose(a1);
plugin.getHandler().updatePlayer(player, luck);
config.setVerbose(a1);
sender.sendMessage(MiniComponent.info("Verbose mode set to " + a1 + "."));
return true;
}
if (args[0].equalsIgnoreCase("reset")) { if (args[0].equalsIgnoreCase("reset")) {
Player player = Bukkit.getPlayer(args[1]); Player player = Bukkit.getPlayer(args[1]);
@ -145,7 +157,7 @@ public class LuckCMD extends Command implements TabCompleter {
if ((sender instanceof Player player) && player.hasPermission("luck.default")) { if ((sender instanceof Player player) && player.hasPermission("luck.default")) {
if (args[0].equalsIgnoreCase("info")) { if (args[0].equalsIgnoreCase("info")) {
Luck luck = plugin.getHandler().getLuckContainer(player); Luck luck = plugin.getHandler().getLuckContainer(player);
player.sendMessage(MiniComponent.info("Your Luck: " + luck.getPercentage())); player.sendMessage(MiniComponent.info("Your Luck: " + luck.getValue()));
return true; return true;
} }
} else if (sender instanceof ConsoleCommandSender) { } else if (sender instanceof ConsoleCommandSender) {
@ -200,4 +212,9 @@ public class LuckCMD extends Command implements TabCompleter {
return completions.stream().filter(n -> n.startsWith(args[0])).toList(); return completions.stream().filter(n -> n.startsWith(args[0])).toList();
} }
@Override
public @NotNull FeelingLucky getPlugin() {
return plugin;
}
} }

View File

@ -0,0 +1,46 @@
package io.github.simplex.luck.util;
import io.github.simplex.lib.MiniComponent;
import io.github.simplex.luck.FeelingLucky;
import org.bukkit.command.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class RegenerateConfigCMD extends Command implements TabCompleter, PluginIdentifiableCommand {
private final FeelingLucky plugin;
public RegenerateConfigCMD(FeelingLucky plugin) {
super("rgc", "Regenerate this plugin's config file.", "/<command>", List.of());
this.plugin = plugin;
setPermission("luck.rgc");
plugin.getCommandMap().register("rgc", "FeelingLucky", this);
plugin.getLogger().info("Successfully registered command: RGC.");
}
@Override
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
sender.sendMessage(MiniComponent.err("This command can only be used through console access."));
return true;
}
plugin.saveResource("config.yml", true);
plugin.getConfig().load();
plugin.getLogger().info("Configuration regenerated.");
return true;
}
@Override
public @NotNull FeelingLucky getPlugin() {
return plugin;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return new ArrayList<>();
}
}

View File

@ -1,6 +1,5 @@
package io.github.simplex.luck.util; package io.github.simplex.luck.util;
import io.github.simplex.luck.listener.AbstractListener;
import io.github.simplex.luck.player.Luck; import io.github.simplex.luck.player.Luck;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Item; import org.bukkit.entity.Item;
@ -22,7 +21,7 @@ public class SneakyWorker {
+ ex.getClass().getSimpleName() + ex.getClass().getSimpleName()
+ " has occurred. A cause will be printed. \n\n" + " has occurred. A cause will be printed. \n\n"
+ ex.getCause(); + ex.getCause();
Bukkit.getLogger().severe(sb); Logs.error(sb);
} }
} }
@ -33,7 +32,7 @@ public class SneakyWorker {
} }
} }
public static Class[] getClasses(String packageName) throws ClassNotFoundException, IOException { public static Class<?>[] getClasses(String packageName) throws ClassNotFoundException, IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
assert classLoader != null; assert classLoader != null;
String path = packageName.replace(".", "/"); String path = packageName.replace(".", "/");
@ -43,11 +42,11 @@ public class SneakyWorker {
URL resource = resources.nextElement(); URL resource = resources.nextElement();
dirs.add(new File(resource.getFile())); dirs.add(new File(resource.getFile()));
} }
ArrayList<Class> classes = new ArrayList<>(); ArrayList<Class<?>> classes = new ArrayList<>();
for (File directory : dirs) { for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName)); classes.addAll(findClasses(directory, packageName));
} }
return classes.toArray(new Class[classes.size()]); return classes.toArray(new Class<?>[0]);
} }
private static List<Class<?>> findClasses(File directory, String packageName) throws ClassNotFoundException { private static List<Class<?>> findClasses(File directory, String packageName) throws ClassNotFoundException {

View File

@ -12,7 +12,7 @@ public class SpecialFootItem {
stack = ItemBuilder.of(Material.RABBIT_FOOT) stack = ItemBuilder.of(Material.RABBIT_FOOT)
.setName("Enhanced Rabbit Foot") .setName("Enhanced Rabbit Foot")
.setAmount(1).setLore("A strange energy radiates from within.", .setAmount(1).setLore("A strange energy radiates from within.",
"This item will increase your luck multiplier by one.") "This item will increase your luck multiplier by 0.1.")
.build(); .build();
} }

View File

@ -0,0 +1,863 @@
/*
* This Metrics class was auto-generated and can be copied into your project if you are
* not using a build tool like Gradle or Maven for dependency management.
*
* IMPORTANT: You are not allowed to modify this class, except changing the package.
*
* Unallowed modifications include but are not limited to:
* - Remove the option for users to opt-out
* - Change the frequency for data submission
* - Obfuscate the code (every obfucator should allow you to make an exception for specific files)
* - Reformat the code (if you use a linter, add an exception)
*
* Violations will result in a ban of your plugin and account from bStats.
*/
package io.github.simplex.metrics;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;
import javax.net.ssl.HttpsURLConnection;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
public class Metrics {
private final Plugin plugin;
private final MetricsBase metricsBase;
/**
* Creates a new Metrics instance.
*
* @param plugin Your plugin instance.
* @param serviceId The id of the service. It can be found at <a
* href="https://bstats.org/what-is-my-plugin-id">What is my plugin id?</a>
*/
public Metrics(JavaPlugin plugin, int serviceId) {
this.plugin = plugin;
// Get the config file
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
File configFile = new File(bStatsFolder, "config.yml");
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
if (!config.isSet("serverUuid")) {
config.addDefault("enabled", true);
config.addDefault("serverUuid", UUID.randomUUID().toString());
config.addDefault("logFailedRequests", false);
config.addDefault("logSentData", false);
config.addDefault("logResponseStatusText", false);
// Inform the server owners about bStats
config
.options()
.header(
"bStats (https://bStats.org) collects some basic information for plugin authors, like how\n"
+ "many people use their plugin and their total player count. It's recommended to keep bStats\n"
+ "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n"
+ "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n"
+ "anonymous.")
.copyDefaults(true);
try {
config.save(configFile);
} catch (IOException ignored) {
}
}
// Load the data
boolean enabled = config.getBoolean("enabled", true);
String serverUUID = config.getString("serverUuid");
boolean logErrors = config.getBoolean("logFailedRequests", false);
boolean logSentData = config.getBoolean("logSentData", false);
boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
metricsBase =
new MetricsBase(
"bukkit",
serverUUID,
serviceId,
enabled,
this::appendPlatformData,
this::appendServiceData,
submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
plugin::isEnabled,
(message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
(message) -> this.plugin.getLogger().log(Level.INFO, message),
logErrors,
logSentData,
logResponseStatusText);
}
/**
* Adds a custom chart.
*
* @param chart The chart to add.
*/
public void addCustomChart(CustomChart chart) {
metricsBase.addCustomChart(chart);
}
private void appendPlatformData(JsonObjectBuilder builder) {
builder.appendField("playerAmount", getPlayerAmount());
builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0);
builder.appendField("bukkitVersion", Bukkit.getVersion());
builder.appendField("bukkitName", Bukkit.getName());
builder.appendField("javaVersion", System.getProperty("java.version"));
builder.appendField("osName", System.getProperty("os.name"));
builder.appendField("osArch", System.getProperty("os.arch"));
builder.appendField("osVersion", System.getProperty("os.version"));
builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
}
private void appendServiceData(JsonObjectBuilder builder) {
builder.appendField("pluginVersion", plugin.getDescription().getVersion());
}
private int getPlayerAmount() {
try {
// Around MC 1.8 the return type was changed from an array to a collection,
// This fixes java.lang.NoSuchMethodError:
// org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
return onlinePlayersMethod.getReturnType().equals(Collection.class)
? ((Collection<?>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
: ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
} catch (Exception e) {
// Just use the new method if the reflection failed
return Bukkit.getOnlinePlayers().size();
}
}
public static class MetricsBase {
/** The version of the Metrics class. */
public static final String METRICS_VERSION = "3.0.0";
private static final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics"));
private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
private final String platform;
private final String serverUuid;
private final int serviceId;
private final Consumer<JsonObjectBuilder> appendPlatformDataConsumer;
private final Consumer<JsonObjectBuilder> appendServiceDataConsumer;
private final Consumer<Runnable> submitTaskConsumer;
private final Supplier<Boolean> checkServiceEnabledSupplier;
private final BiConsumer<String, Throwable> errorLogger;
private final Consumer<String> infoLogger;
private final boolean logErrors;
private final boolean logSentData;
private final boolean logResponseStatusText;
private final Set<CustomChart> customCharts = new HashSet<>();
private final boolean enabled;
/**
* Creates a new MetricsBase class instance.
*
* @param platform The platform of the service.
* @param serviceId The id of the service.
* @param serverUuid The server uuid.
* @param enabled Whether or not data sending is enabled.
* @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
* appends all platform-specific data.
* @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
* appends all service-specific data.
* @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be
* used to delegate the data collection to a another thread to prevent errors caused by
* concurrency. Can be {@code null}.
* @param checkServiceEnabledSupplier A supplier to check if the service is still enabled.
* @param errorLogger A consumer that accepts log message and an error.
* @param infoLogger A consumer that accepts info log messages.
* @param logErrors Whether or not errors should be logged.
* @param logSentData Whether or not the sent data should be logged.
* @param logResponseStatusText Whether or not the response status text should be logged.
*/
public MetricsBase(
String platform,
String serverUuid,
int serviceId,
boolean enabled,
Consumer<JsonObjectBuilder> appendPlatformDataConsumer,
Consumer<JsonObjectBuilder> appendServiceDataConsumer,
Consumer<Runnable> submitTaskConsumer,
Supplier<Boolean> checkServiceEnabledSupplier,
BiConsumer<String, Throwable> errorLogger,
Consumer<String> infoLogger,
boolean logErrors,
boolean logSentData,
boolean logResponseStatusText) {
this.platform = platform;
this.serverUuid = serverUuid;
this.serviceId = serviceId;
this.enabled = enabled;
this.appendPlatformDataConsumer = appendPlatformDataConsumer;
this.appendServiceDataConsumer = appendServiceDataConsumer;
this.submitTaskConsumer = submitTaskConsumer;
this.checkServiceEnabledSupplier = checkServiceEnabledSupplier;
this.errorLogger = errorLogger;
this.infoLogger = infoLogger;
this.logErrors = logErrors;
this.logSentData = logSentData;
this.logResponseStatusText = logResponseStatusText;
checkRelocation();
if (enabled) {
// WARNING: Removing the option to opt-out will get your plugin banned from bStats
startSubmitting();
}
}
public void addCustomChart(CustomChart chart) {
this.customCharts.add(chart);
}
private void startSubmitting() {
final Runnable submitTask =
() -> {
if (!enabled || !checkServiceEnabledSupplier.get()) {
// Submitting data or service is disabled
scheduler.shutdown();
return;
}
if (submitTaskConsumer != null) {
submitTaskConsumer.accept(this::submitData);
} else {
this.submitData();
}
};
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution
// of requests on the
// bStats backend. To circumvent this problem, we introduce some randomness into the initial
// and second delay.
// WARNING: You must not modify and part of this Metrics class, including the submit delay or
// frequency!
// WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
scheduler.scheduleAtFixedRate(
submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
}
private void submitData() {
final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder();
appendPlatformDataConsumer.accept(baseJsonBuilder);
final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder();
appendServiceDataConsumer.accept(serviceJsonBuilder);
JsonObjectBuilder.JsonObject[] chartData =
customCharts.stream()
.map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors))
.filter(Objects::nonNull)
.toArray(JsonObjectBuilder.JsonObject[]::new);
serviceJsonBuilder.appendField("id", serviceId);
serviceJsonBuilder.appendField("customCharts", chartData);
baseJsonBuilder.appendField("service", serviceJsonBuilder.build());
baseJsonBuilder.appendField("serverUUID", serverUuid);
baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION);
JsonObjectBuilder.JsonObject data = baseJsonBuilder.build();
scheduler.execute(
() -> {
try {
// Send the data
sendData(data);
} catch (Exception e) {
// Something went wrong! :(
if (logErrors) {
errorLogger.accept("Could not submit bStats metrics data", e);
}
}
});
}
private void sendData(JsonObjectBuilder.JsonObject data) throws Exception {
if (logSentData) {
infoLogger.accept("Sent bStats metrics data: " + data.toString());
}
String url = String.format(REPORT_URL, platform);
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
// Compress the data to save bandwidth
byte[] compressedData = compress(data.toString());
connection.setRequestMethod("POST");
connection.addRequestProperty("Accept", "application/json");
connection.addRequestProperty("Connection", "close");
connection.addRequestProperty("Content-Encoding", "gzip");
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", "Metrics-Service/1");
connection.setDoOutput(true);
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
outputStream.write(compressedData);
}
StringBuilder builder = new StringBuilder();
try (BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
builder.append(line);
}
}
if (logResponseStatusText) {
infoLogger.accept("Sent data to bStats and received response: " + builder);
}
}
/** Checks that the class was properly relocated. */
private void checkRelocation() {
// You can use the property to disable the check in your test environment
if (System.getProperty("bstats.relocatecheck") == null
|| !System.getProperty("bstats.relocatecheck").equals("false")) {
// Maven's Relocate is clever and changes strings, too. So we have to use this little
// "trick" ... :D
final String defaultPackage =
new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
final String examplePackage =
new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
// We want to make sure no one just copy & pastes the example and uses the wrong package
// names
if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
|| MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
}
}
}
/**
* Gzips the given string.
*
* @param str The string to gzip.
* @return The gzipped string.
*/
private static byte[] compress(final String str) throws IOException {
if (str == null) {
return null;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
gzip.write(str.getBytes(StandardCharsets.UTF_8));
}
return outputStream.toByteArray();
}
}
public static class DrilldownPie extends CustomChart {
private final Callable<Map<String, Map<String, Integer>>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
super(chartId);
this.callable = callable;
}
@Override
public JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Map<String, Integer>> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean reallyAllSkipped = true;
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
JsonObjectBuilder valueBuilder = new JsonObjectBuilder();
boolean allSkipped = true;
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue());
allSkipped = false;
}
if (!allSkipped) {
reallyAllSkipped = false;
valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build());
}
}
if (reallyAllSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class AdvancedPie extends CustomChart {
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class MultiLineChart extends CustomChart {
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class SimpleBarChart extends CustomChart {
private final Callable<Map<String, Integer>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
for (Map.Entry<String, Integer> entry : map.entrySet()) {
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public abstract static class CustomChart {
private final String chartId;
protected CustomChart(String chartId) {
if (chartId == null) {
throw new IllegalArgumentException("chartId must not be null");
}
this.chartId = chartId;
}
public JsonObjectBuilder.JsonObject getRequestJsonObject(
BiConsumer<String, Throwable> errorLogger, boolean logErrors) {
JsonObjectBuilder builder = new JsonObjectBuilder();
builder.appendField("chartId", chartId);
try {
JsonObjectBuilder.JsonObject data = getChartData();
if (data == null) {
// If the data is null we don't send the chart.
return null;
}
builder.appendField("data", data);
} catch (Throwable t) {
if (logErrors) {
errorLogger.accept("Failed to get data for custom chart with id " + chartId, t);
}
return null;
}
return builder.build();
}
protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception;
}
public static class SimplePie extends CustomChart {
private final Callable<String> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SimplePie(String chartId, Callable<String> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
String value = callable.call();
if (value == null || value.isEmpty()) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("value", value).build();
}
}
public static class AdvancedBarChart extends CustomChart {
private final Callable<Map<String, int[]>> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
Map<String, int[]> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, int[]> entry : map.entrySet()) {
if (entry.getValue().length == 0) {
// Skip this invalid
continue;
}
allSkipped = false;
valuesBuilder.appendField(entry.getKey(), entry.getValue());
}
if (allSkipped) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
}
}
public static class SingleLineChart extends CustomChart {
private final Callable<Integer> callable;
/**
* Class constructor.
*
* @param chartId The id of the chart.
* @param callable The callable which is used to request the chart data.
*/
public SingleLineChart(String chartId, Callable<Integer> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
int value = callable.call();
if (value == 0) {
// Null = skip the chart
return null;
}
return new JsonObjectBuilder().appendField("value", value).build();
}
}
/**
* An extremely simple JSON builder.
*
* <p>While this class is neither feature-rich nor the most performant one, it's sufficient enough
* for its use-case.
*/
public static class JsonObjectBuilder {
private StringBuilder builder = new StringBuilder();
private boolean hasAtLeastOneField = false;
public JsonObjectBuilder() {
builder.append("{");
}
/**
* Appends a null field to the JSON.
*
* @param key The key of the field.
* @return A reference to this object.
*/
public JsonObjectBuilder appendNull(String key) {
appendFieldUnescaped(key, "null");
return this;
}
/**
* Appends a string field to the JSON.
*
* @param key The key of the field.
* @param value The value of the field.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, String value) {
if (value == null) {
throw new IllegalArgumentException("JSON value must not be null");
}
appendFieldUnescaped(key, "\"" + escape(value) + "\"");
return this;
}
/**
* Appends an integer field to the JSON.
*
* @param key The key of the field.
* @param value The value of the field.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, int value) {
appendFieldUnescaped(key, String.valueOf(value));
return this;
}
/**
* Appends an object to the JSON.
*
* @param key The key of the field.
* @param object The object.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, JsonObject object) {
if (object == null) {
throw new IllegalArgumentException("JSON object must not be null");
}
appendFieldUnescaped(key, object.toString());
return this;
}
/**
* Appends a string array to the JSON.
*
* @param key The key of the field.
* @param values The string array.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, String[] values) {
if (values == null) {
throw new IllegalArgumentException("JSON values must not be null");
}
String escapedValues =
Arrays.stream(values)
.map(value -> "\"" + escape(value) + "\"")
.collect(Collectors.joining(","));
appendFieldUnescaped(key, "[" + escapedValues + "]");
return this;
}
/**
* Appends an integer array to the JSON.
*
* @param key The key of the field.
* @param values The integer array.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, int[] values) {
if (values == null) {
throw new IllegalArgumentException("JSON values must not be null");
}
String escapedValues =
Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(","));
appendFieldUnescaped(key, "[" + escapedValues + "]");
return this;
}
/**
* Appends an object array to the JSON.
*
* @param key The key of the field.
* @param values The integer array.
* @return A reference to this object.
*/
public JsonObjectBuilder appendField(String key, JsonObject[] values) {
if (values == null) {
throw new IllegalArgumentException("JSON values must not be null");
}
String escapedValues =
Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(","));
appendFieldUnescaped(key, "[" + escapedValues + "]");
return this;
}
/**
* Appends a field to the object.
*
* @param key The key of the field.
* @param escapedValue The escaped value of the field.
*/
private void appendFieldUnescaped(String key, String escapedValue) {
if (builder == null) {
throw new IllegalStateException("JSON has already been built");
}
if (key == null) {
throw new IllegalArgumentException("JSON key must not be null");
}
if (hasAtLeastOneField) {
builder.append(",");
}
builder.append("\"").append(escape(key)).append("\":").append(escapedValue);
hasAtLeastOneField = true;
}
/**
* Builds the JSON string and invalidates this builder.
*
* @return The built JSON string.
*/
public JsonObject build() {
if (builder == null) {
throw new IllegalStateException("JSON has already been built");
}
JsonObject object = new JsonObject(builder.append("}").toString());
builder = null;
return object;
}
/**
* Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt.
*
* <p>This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'.
* Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n").
*
* @param value The value to escape.
* @return The escaped value.
*/
private static String escape(String value) {
final StringBuilder builder = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c == '"') {
builder.append("\\\"");
} else if (c == '\\') {
builder.append("\\\\");
} else if (c <= '\u000F') {
builder.append("\\u000").append(Integer.toHexString(c));
} else if (c <= '\u001F') {
builder.append("\\u00").append(Integer.toHexString(c));
} else {
builder.append(c);
}
}
return builder.toString();
}
/**
* A super simple representation of a JSON object.
*
* <p>This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not
* allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String,
* JsonObject)}.
*/
public static class JsonObject {
private final String value;
private JsonObject(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}
}
}

View File

@ -19,7 +19,11 @@ bonemeal: MED
cheat_death: MED cheat_death: MED
enchanting: HIGH enchanting: HIGH
experience: HIGH experience: HIGH
give_damage: LOW
hide_check: MED
item_drops: LOW item_drops: LOW
jump_boost: MED
ore_vein: HIGH
random_effect: HIGH random_effect: HIGH
restore_hunger: NONE restore_hunger: NONE
take_damage: MED take_damage: MED