diff --git a/.github/configs/yaml-linter.yml b/.github/configs/yaml-linter.yml index 90060a83c..3f056f2a1 100644 --- a/.github/configs/yaml-linter.yml +++ b/.github/configs/yaml-linter.yml @@ -6,10 +6,13 @@ yaml-files: rules: - ## A warning is sufficient here - line-length: - max: 180 - level: warning + ## Don't warn for line lengths + line-length: disable + + truthy: + allowed-values: ['true', 'false'] + ## We don't want it to trigger for the 'on' in our workflows + check-keys: false ## We don't need indentation warnings indentation: disable diff --git a/.github/workflows/url-checker.yml b/.github/workflows/url-checker.yml index 523248f00..42483c750 100644 --- a/.github/workflows/url-checker.yml +++ b/.github/workflows/url-checker.yml @@ -6,7 +6,7 @@ on: - master jobs: - build: + check: name: URL Checker runs-on: ubuntu-latest @@ -17,7 +17,7 @@ jobs: with: git_path: https://github.com/Slimefun/Slimefun4 file_types: .md,.java,.yml - print_all: False + print_all: false retry_count: 2 ## These URLs will always be correct, even if their services may be offline right now white_listed_patterns: http://textures.minecraft.net/texture/,https://pastebin.com/,https://www.spigotmc.org/threads/spigot-bungeecord-1-16-1.447405/#post-3852349,https://gitlocalize.com/repo/3841 diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml index a1a4f8e88..2c34456f9 100644 --- a/.github/workflows/yaml-linter.yml +++ b/.github/workflows/yaml-linter.yml @@ -18,6 +18,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - name: YAML Linter - uses: ibiqlik/action-yamllint@v1.0.0 + uses: ibiqlik/action-yamllint@v2.0.0 with: config_file: '.github/configs/yaml-linter.yml' diff --git a/CHANGELOG.md b/CHANGELOG.md index ed550b2c5..3bee33d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,12 @@ #### Changes * Removed 1.13 support +* Cooling Units can no longer be placed down +* Heating Coils can no longer be placed down +* Electric Motors can no longer be placed down +* Cargo Motors can no longer be placed down +* Magnets can no longer be placed down +* Electromagnets can no longer be placed down #### Fixes * Fixed #2448 @@ -36,6 +42,11 @@ * Fixed #2478 * Fixed #2493 * Fixed a missing slot in the contributors menu +* Fixed color codes in script downloading screen +* Fixed #2505 +* Fixed contributors not showing correctly +* Fixed #2469 +* Fixed #2509 ## Release Candidate 17 (17 Oct 2020) diff --git a/pom.xml b/pom.xml index 6334ca848..0073cdbbc 100644 --- a/pom.xml +++ b/pom.xml @@ -329,7 +329,7 @@ org.mockito mockito-core - 3.5.15 + 3.6.0 test @@ -381,7 +381,7 @@ com.gmail.nossr50.mcMMO mcMMO - 2.1.149 + 2.1.150 provided diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/BlockDataService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/BlockDataService.java index c9c577681..3e2ac719b 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/BlockDataService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/BlockDataService.java @@ -2,6 +2,9 @@ package io.github.thebusybiscuit.slimefun4.core.services; import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import org.bukkit.Keyed; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -11,6 +14,9 @@ import org.bukkit.block.TileState; import org.bukkit.persistence.PersistentDataHolder; import org.bukkit.plugin.Plugin; +import io.papermc.lib.PaperLib; +import io.papermc.lib.features.blockstatesnapshot.BlockStateSnapshotResult; + /** * The {@link BlockDataService} is similar to the {@link CustomItemDataService}, * it is responsible for storing NBT data inside a {@link TileState}. @@ -24,7 +30,17 @@ public class BlockDataService implements PersistentDataService, Keyed { private final NamespacedKey namespacedKey; - public BlockDataService(Plugin plugin, String key) { + /** + * This creates a new {@link BlockDataService} for the given {@link Plugin}. + * The {@link Plugin} and key will together form a {@link NamespacedKey} used to store + * data on a {@link TileState}. + * + * @param plugin + * The {@link Plugin} responsible for this service + * @param key + * The key under which to store data + */ + public BlockDataService(@Nonnull Plugin plugin, @Nonnull String key) { namespacedKey = new NamespacedKey(plugin, key); } @@ -41,12 +57,16 @@ public class BlockDataService implements PersistentDataService, Keyed { * @param value * The value to store */ - public void setBlockData(Block b, String value) { - BlockState state = b.getState(); + public void setBlockData(@Nonnull Block b, @Nonnull String value) { + BlockStateSnapshotResult result = PaperLib.getBlockState(b, false); + BlockState state = result.getState(); if (state instanceof TileState) { setString((TileState) state, namespacedKey, value); - state.update(); + + if (result.isSnapshot()) { + state.update(); + } } } @@ -57,8 +77,8 @@ public class BlockDataService implements PersistentDataService, Keyed { * The {@link Block} to retrieve data from * @return The stored value */ - public Optional getBlockData(Block b) { - BlockState state = b.getState(); + public Optional getBlockData(@Nonnull Block b) { + BlockState state = PaperLib.getBlockState(b, false).getState(); if (state instanceof TileState) { return getString((TileState) state, namespacedKey); @@ -77,9 +97,10 @@ public class BlockDataService implements PersistentDataService, Keyed { * * @param type * The {@link Material} to check for + * * @return Whether the given {@link Material} is considered a Tile Entity */ - public boolean isTileEntity(Material type) { + public boolean isTileEntity(@Nullable Material type) { if (type == null || type.isAir()) { // Cannot store data on air return false; @@ -104,8 +125,10 @@ public class BlockDataService implements PersistentDataService, Keyed { case BARREL: case SPAWNER: case BEACON: + // All of the above Materials are Tile Entities return true; default: + // Otherwise we assume they're not Tile Entities return false; } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomItemDataService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomItemDataService.java index b2ca07a7c..aed4e5d3e 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomItemDataService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomItemDataService.java @@ -2,6 +2,10 @@ package io.github.thebusybiscuit.slimefun4.core.services; import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.commons.lang.Validate; import org.bukkit.Keyed; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -35,7 +39,7 @@ public class CustomItemDataService implements PersistentDataService, Keyed { * @param key * The key under which to store data */ - public CustomItemDataService(Plugin plugin, String key) { + public CustomItemDataService(@Nonnull Plugin plugin, @Nonnull String key) { // Null-Validation is performed in the NamespacedKey constructor namespacedKey = new NamespacedKey(plugin, key); } @@ -45,13 +49,37 @@ public class CustomItemDataService implements PersistentDataService, Keyed { return namespacedKey; } - public void setItemData(ItemStack item, String id) { + /** + * This method stores the given id on the provided {@link ItemStack} via + * persistent data. + * + * @param item + * The {@link ItemStack} to store data on + * @param id + * The id to store on the {@link ItemStack} + */ + public void setItemData(@Nonnull ItemStack item, @Nonnull String id) { + Validate.notNull(item, "The Item cannot be null!"); + Validate.notNull(id, "Cannot store null on an Item!"); + ItemMeta im = item.getItemMeta(); setItemData(im, id); item.setItemMeta(im); } - public void setItemData(ItemMeta im, String id) { + /** + * This method stores the given id on the provided {@link ItemMeta} via + * persistent data. + * + * @param im + * The {@link ItemMeta} to store data on + * @param id + * The id to store on the {@link ItemMeta} + */ + public void setItemData(@Nonnull ItemMeta im, @Nonnull String id) { + Validate.notNull(im, "The ItemMeta cannot be null!"); + Validate.notNull(id, "Cannot store null on an ItemMeta!"); + setString(im, namespacedKey, id); } @@ -65,7 +93,8 @@ public class CustomItemDataService implements PersistentDataService, Keyed { * * @return An {@link Optional} describing the result */ - public Optional getItemData(ItemStack item) { + @Nonnull + public Optional getItemData(@Nullable ItemStack item) { if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) { return Optional.empty(); } @@ -82,7 +111,10 @@ public class CustomItemDataService implements PersistentDataService, Keyed { * * @return An {@link Optional} describing the result */ - public Optional getItemData(ItemMeta meta) { + @Nonnull + public Optional getItemData(@Nonnull ItemMeta meta) { + Validate.notNull(meta, "Cannot read data from null!"); + return getString(meta, namespacedKey); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomTextureService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomTextureService.java index 5587ad8b2..3caea1cbb 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomTextureService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/CustomTextureService.java @@ -28,6 +28,12 @@ public class CustomTextureService { private String version = null; private boolean modified = false; + /** + * This creates a new {@link CustomTextureService} for the provided {@link Config} + * + * @param config + * The {@link Config} to read custom model data from + */ public CustomTextureService(@Nonnull Config config) { this.config = config; config.getConfiguration().options().header("This file is used to assign items from Slimefun or any of its addons\n" + "the 'CustomModelData' NBT tag. This can be used in conjunction with a custom resource pack\n" + "to give items custom textures.\n0 means there is no data assigned to that item.\n\n" + "There is no official Slimefun resource pack at the moment."); @@ -49,6 +55,8 @@ public class CustomTextureService { config.setDefaultValue("SLIMEFUN_GUIDE", 0); config.setDefaultValue("_UI_BACKGROUND", 0); + config.setDefaultValue("_UI_NO_PERMISSION", 0); + config.setDefaultValue("_UI_NOT_RESEARCHED", 0); config.setDefaultValue("_UI_INPUT_SLOT", 0); config.setDefaultValue("_UI_OUTPUT_SLOT", 0); config.setDefaultValue("_UI_BACK", 0); @@ -82,22 +90,60 @@ public class CustomTextureService { return version; } + /** + * This returns true if any custom model data was configured. + * If every item id has no configured custom model data, it will return false. + * + * @return Whether any custom model data was configured + */ public boolean isActive() { return modified; } + /** + * This returns the configured custom model data for a given id. + * + * @param id + * The id to get the data for + * + * @return The configured custom model data + */ public int getModelData(@Nonnull String id) { Validate.notNull(id, "Cannot get the ModelData for 'null'"); return config.getInt(id); } + /** + * This method sets the custom model data for this {@link ItemStack} + * to the value configured for the provided item id. + * + * @param item + * The {@link ItemStack} to set the custom model data for + * @param id + * The id for which to get the configured model data + */ public void setTexture(@Nonnull ItemStack item, @Nonnull String id) { + Validate.notNull(item, "The Item cannot be null!"); + Validate.notNull(id, "Cannot store null on an Item!"); + ItemMeta im = item.getItemMeta(); setTexture(im, id); item.setItemMeta(im); } + /** + * This method sets the custom model data for this {@link ItemMeta} + * to the value configured for the provided item id. + * + * @param im + * The {@link ItemMeta} to set the custom model data for + * @param id + * The id for which to get the configured model data + */ public void setTexture(@Nonnull ItemMeta im, @Nonnull String id) { + Validate.notNull(im, "The ItemMeta cannot be null!"); + Validate.notNull(id, "Cannot store null on an ItemMeta!"); + int data = getModelData(id); im.setCustomModelData(data == 0 ? null : data); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/MetricsService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/MetricsService.java index 5835e9f86..74ddcd2e1 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/MetricsService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/MetricsService.java @@ -34,9 +34,26 @@ import kong.unirest.UnirestException; */ public class MetricsService { + /** + * The URL pointing towards the GitHub API. + */ private static final String API_URL = "https://api.github.com/"; + + /** + * The Name of our repository + */ private static final String REPO_NAME = "MetricsModule"; + + /** + * The URL pointing towards the /releases/ endpoint of our + * Metrics repository + */ private static final String RELEASES_URL = API_URL + "repos/Slimefun/" + REPO_NAME + "/releases/latest"; + + /** + * The URL pointing towards the download location for a + * GitHub release of our Metrics repository + */ private static final String DOWNLOAD_URL = "https://github.com/Slimefun/" + REPO_NAME + "/releases/download"; private final SlimefunPlugin plugin; @@ -48,9 +65,22 @@ public class MetricsService { private boolean hasDownloadedUpdate = false; static { - Unirest.config().concurrency(2, 1).setDefaultHeader("User-Agent", "MetricsModule Auto-Updater").setDefaultHeader("Accept", "application/vnd.github.v3+json").enableCookieManagement(false).cookieSpec("ignoreCookies"); + // @formatter:off (We want this to stay this nicely aligned :D ) + Unirest.config() + .concurrency(2, 1) + .setDefaultHeader("User-Agent", "MetricsModule Auto-Updater") + .setDefaultHeader("Accept", "application/vnd.github.v3+json") + .enableCookieManagement(false) + .cookieSpec("ignoreCookies"); + // @formatter:on } + /** + * This constructs a new instance of our {@link MetricsService}. + * + * @param plugin + * Our {@link SlimefunPlugin} instance + */ public MetricsService(@Nonnull SlimefunPlugin plugin) { this.plugin = plugin; this.parentFolder = new File(plugin.getDataFolder(), "cache" + File.separatorChar + "modules"); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/ContributionsConnector.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/ContributionsConnector.java index f38e0c035..3d2368cea 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/ContributionsConnector.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/ContributionsConnector.java @@ -16,11 +16,28 @@ import me.mrCookieSlime.Slimefun.api.Slimefun; class ContributionsConnector extends GitHubConnector { - // GitHub Bots that do not count as Contributors - // (includes "invalid-email-address" because it is an invalid contributor) - private static final List blacklist = Arrays.asList("invalid-email-address", "renovate-bot", "TheBusyBot", "ImgBotApp", "imgbot", "imgbot[bot]", "github-actions[bot]", "gitlocalize-app", "gitlocalize-app[bot]", "mt-gitlocalize"); + /* + * @formatter:off + * GitHub Bots that do not count as Contributors + * (includes "invalid-email-address" because it is an invalid contributor) + */ + private static final List blacklist = Arrays.asList( + "invalid-email-address", + "renovate-bot", + "TheBusyBot", + "ImgBotApp", + "imgbot", + "imgbot[bot]", + "github-actions[bot]", + "gitlocalize-app", + "gitlocalize-app[bot]", + "mt-gitlocalize" + ); - // Matches a GitHub name with a Minecraft name. + /* + * @formatter:on + * Matches a GitHub name with a Minecraft name. + */ private static final Map aliases = new HashMap<>(); // Should probably be switched to UUIDs at some point... @@ -82,8 +99,16 @@ class ContributionsConnector extends GitHubConnector { } @Override - public String getURLSuffix() { - return "/contributors?per_page=100&page=" + page; + public String getEndpoint() { + return "/contributors"; + } + + @Override + public Map getParameters() { + Map parameters = new HashMap<>(); + parameters.put("per_page", 100); + parameters.put("page", page); + return parameters; } private void computeContributors(@Nonnull JSONArray array) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubActivityConnector.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubActivityConnector.java index e55fc722e..33b6d23e1 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubActivityConnector.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubActivityConnector.java @@ -1,6 +1,8 @@ package io.github.thebusybiscuit.slimefun4.core.services.github; import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; @@ -35,8 +37,13 @@ class GitHubActivityConnector extends GitHubConnector { } @Override - public String getURLSuffix() { + public String getEndpoint() { return ""; } + @Override + public Map getParameters() { + return new HashMap<>(); + } + } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubConnector.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubConnector.java index 0f9e3e317..e06e00129 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubConnector.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubConnector.java @@ -6,12 +6,13 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import javax.annotation.ParametersAreNonnullByDefault; import kong.unirest.HttpResponse; import kong.unirest.JsonNode; @@ -31,22 +32,49 @@ import me.mrCookieSlime.Slimefun.api.Slimefun; abstract class GitHubConnector { private static final String API_URL = "https://api.github.com/"; + private static final String USER_AGENT = "Slimefun4 (https://github.com/Slimefun)"; - protected File file; - protected String repository; protected final GitHubService github; + private final String url; + private File file; - @ParametersAreNonnullByDefault - public GitHubConnector(GitHubService github, String repository) { + /** + * This creates a new {@link GitHubConnector} for the given repository. + * + * @param github + * Our instance of {@link GitHubService} + * @param repository + * The repository we want to connect to + */ + GitHubConnector(@Nonnull GitHubService github, @Nonnull String repository) { this.github = github; - this.repository = repository; + this.url = API_URL + "repos/" + repository + getEndpoint(); } + /** + * This returns the name of our cache {@link File}. + * + * @return The cache {@link File} name + */ @Nonnull public abstract String getFileName(); + /** + * This is our {@link URL} endpoint. + * It is the suffix of the {@link URL} we want to connect to. + * + * @return Our endpoint + */ @Nonnull - public abstract String getURLSuffix(); + public abstract String getEndpoint(); + + /** + * This {@link Map} contains the query parameters for our {@link URL}. + * + * @return A {@link Map} with our query parameters + */ + @Nonnull + public abstract Map getParameters(); /** * This method is called when the connection finished successfully. @@ -63,7 +91,12 @@ abstract class GitHubConnector { // Don't do anything by default } - public void pullFile() { + /** + * This method will connect to GitHub and store the received data inside a local + * cache {@link File}. + * Make sure to call this method asynchronously! + */ + void download() { file = new File("plugins/Slimefun/cache/github/" + getFileName() + ".json"); if (github.isLoggingEnabled()) { @@ -71,16 +104,19 @@ abstract class GitHubConnector { } try { - HttpResponse resp = Unirest.get(API_URL + "repos/" + repository + getURLSuffix()) - .header("User-Agent", "Slimefun4 (https://github.com/Slimefun)") + // @formatter:off + HttpResponse response = Unirest.get(url) + .queryString(getParameters()) + .header("User-Agent", USER_AGENT) .asJson(); + // @formatter:on - if (resp.isSuccess()) { - onSuccess(resp.getBody()); - writeCacheFile(resp.getBody()); + if (response.isSuccess()) { + onSuccess(response.getBody()); + writeCacheFile(response.getBody()); } else { if (github.isLoggingEnabled()) { - Slimefun.getLogger().log(Level.WARNING, "Failed to fetch {0}: {1} - {2}", new Object[] { repository + getURLSuffix(), resp.getStatus(), resp.getBody() }); + Slimefun.getLogger().log(Level.WARNING, "Failed to fetch {0}: {1} - {2}", new Object[] { url, response.getStatus(), response.getBody() }); } // It has the cached file, let's just read that then diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubIssuesConnector.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubIssuesConnector.java index b22d9f491..38335e529 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubIssuesConnector.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubIssuesConnector.java @@ -1,5 +1,7 @@ package io.github.thebusybiscuit.slimefun4.core.services.github; +import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; import javax.annotation.Nonnull; @@ -50,8 +52,15 @@ class GitHubIssuesConnector extends GitHubConnector { } @Override - public String getURLSuffix() { - return "/issues?per_page=100"; + public String getEndpoint() { + return "/issues"; + } + + @Override + public Map getParameters() { + Map parameters = new HashMap<>(); + parameters.put("per_page", 100); + return parameters; } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubService.java index b7a2fc07d..f7d103f24 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubService.java @@ -57,7 +57,8 @@ public class GitHubService { } public void start(@Nonnull SlimefunPlugin plugin) { - plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new GitHubTask(this), 80L, 60 * 60 * 20L); + GitHubTask task = new GitHubTask(this); + plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, task, 80L, 60 * 60 * 20L); } /** diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java index 932bf7060..151afa635 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java @@ -1,5 +1,6 @@ package io.github.thebusybiscuit.slimefun4.core.services.github; +import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -38,10 +39,14 @@ class GitHubTask implements Runnable { @Override public void run() { - gitHubService.getConnectors().forEach(GitHubConnector::pullFile); + gitHubService.getConnectors().forEach(GitHubConnector::download); grabTextures(); } + /** + * This method will pull the skin textures for every {@link Contributor} and store + * the {@link UUID} and received skin inside a local cache {@link File}. + */ private void grabTextures() { // Store all queried usernames to prevent 429 responses for pinging the // same URL twice in one run. diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/ChestSlimefunGuide.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/ChestSlimefunGuide.java index e024b4c38..d88a295bf 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/ChestSlimefunGuide.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/ChestSlimefunGuide.java @@ -271,10 +271,10 @@ public class ChestSlimefunGuide implements SlimefunGuideImplementation { if (isSurvivalMode() && !Slimefun.hasPermission(p, sfitem, false)) { List message = SlimefunPlugin.getPermissionsService().getLore(sfitem); - menu.addItem(index, new CustomItem(Material.BARRIER, sfitem.getItemName(), message.toArray(new String[0]))); + menu.addItem(index, new CustomItem(ChestMenuUtils.getNoPermissionItem(), sfitem.getItemName(), message.toArray(new String[0]))); menu.addMenuClickHandler(index, ChestMenuUtils.getEmptyClickHandler()); } else if (isSurvivalMode() && research != null && !profile.hasUnlocked(research)) { - menu.addItem(index, new CustomItem(Material.BARRIER, ChatColor.WHITE + ItemUtils.getItemName(sfitem.getItem()), "&4&l" + SlimefunPlugin.getLocalization().getMessage(p, "guide.locked"), "", "&a> Click to unlock", "", "&7Cost: &b" + research.getCost() + " Level(s)")); + menu.addItem(index, new CustomItem(ChestMenuUtils.getNotResearchedItem(), ChatColor.WHITE + ItemUtils.getItemName(sfitem.getItem()), "&4&l" + SlimefunPlugin.getLocalization().getMessage(p, "guide.locked"), "", "&a> Click to unlock", "", "&7Cost: &b" + research.getCost() + " Level(s)")); menu.addMenuClickHandler(index, (pl, slot, item, action) -> { if (!SlimefunPlugin.getRegistry().getCurrentlyResearchingPlayers().contains(pl.getUniqueId())) { if (research.canUnlock(pl)) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/AndroidType.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/AndroidType.java index 61e7a2eaa..732c998a3 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/AndroidType.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/AndroidType.java @@ -1,5 +1,7 @@ package io.github.thebusybiscuit.slimefun4.implementation.items.androids; +import javax.annotation.Nonnull; + /** * This enum holds all the different types a {@link ProgrammableAndroid} can represent. * @@ -55,7 +57,7 @@ public enum AndroidType { */ NON_FIGHTER; - boolean isType(AndroidType type) { + boolean isType(@Nonnull AndroidType type) { return type == NONE || type == this || (type == NON_FIGHTER && this != FIGHTER); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Instruction.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Instruction.java index 4a2083b05..6c707c979 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Instruction.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Instruction.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.function.Predicate; import org.apache.commons.lang.Validate; +import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Ageable; @@ -20,126 +21,223 @@ import io.github.thebusybiscuit.slimefun4.utils.HeadTexture; import io.github.thebusybiscuit.slimefun4.utils.SlimefunUtils; import me.mrCookieSlime.Slimefun.api.inventory.BlockMenu; -enum Instruction { +/** + * This enum holds every {@link Instruction} for the {@link ProgrammableAndroid} + * added by Slimefun itself. + * + * @author TheBusyBiscuit + * + */ +public enum Instruction { - // Start and End Parts + /** + * This {@link Instruction} is the starting point of a {@link Script}. + */ START(AndroidType.NONE, HeadTexture.SCRIPT_START), + + /** + * This {@link Instruction} is the end token of a {@link Script}. + * Once this {@link Instruction} is reached, the {@link Script} will start again. + */ REPEAT(AndroidType.NONE, HeadTexture.SCRIPT_REPEAT), + + /** + * This {@link Instruction} will make the {@link ProgrammableAndroid} wait + * for one Slimefun tick. + */ WAIT(AndroidType.NONE, HeadTexture.SCRIPT_WAIT), - // Movement + /** + * This will make the {@link ProgrammableAndroid} go forward. + */ GO_FORWARD(AndroidType.NON_FIGHTER, HeadTexture.SCRIPT_FORWARD, (android, b, inv, face) -> { Block target = b.getRelative(face); android.move(b, face, target); }), + /** + * This will make the {@link ProgrammableAndroid} go up. + */ GO_UP(AndroidType.NON_FIGHTER, HeadTexture.SCRIPT_UP, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.UP); android.move(b, face, target); }), + /** + * This will make the {@link ProgrammableAndroid} go down. + */ GO_DOWN(AndroidType.NON_FIGHTER, HeadTexture.SCRIPT_DOWN, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.DOWN); android.move(b, face, target); }), - // Directions + /** + * This will make the {@link ProgrammableAndroid} rotate to the left side. + */ TURN_LEFT(AndroidType.NONE, HeadTexture.SCRIPT_LEFT, (android, b, inv, face) -> { int mod = -1; android.rotate(b, face, mod); }), + /** + * This will make the {@link ProgrammableAndroid} rotate to the right side. + */ TURN_RIGHT(AndroidType.NONE, HeadTexture.SCRIPT_RIGHT, (android, b, inv, face) -> { int mod = 1; android.rotate(b, face, mod); }), - // Action - Pickaxe + /** + * This will make a {@link MinerAndroid} dig the {@link Block} above. + */ DIG_UP(AndroidType.MINER, HeadTexture.SCRIPT_DIG_UP, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.UP); android.dig(b, inv, target); }), + /** + * This will make a {@link MinerAndroid} dig the {@link Block} ahead. + */ DIG_FORWARD(AndroidType.MINER, HeadTexture.SCRIPT_DIG_FORWARD, (android, b, inv, face) -> { Block target = b.getRelative(face); android.dig(b, inv, target); }), + /** + * This will make a {@link MinerAndroid} dig the {@link Block} below. + */ DIG_DOWN(AndroidType.MINER, HeadTexture.SCRIPT_DIG_DOWN, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.DOWN); android.dig(b, inv, target); }), + /** + * This will make a {@link MinerAndroid} dig the {@link Block} above + * and then move itself to that new {@link Location}. + */ MOVE_AND_DIG_UP(AndroidType.MINER, HeadTexture.SCRIPT_DIG_UP, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.UP); android.moveAndDig(b, inv, face, target); }), + /** + * This will make a {@link MinerAndroid} dig the {@link Block} ahead + * and then move itself to that new {@link Location}. + */ MOVE_AND_DIG_FORWARD(AndroidType.MINER, HeadTexture.SCRIPT_DIG_FORWARD, (android, b, inv, face) -> { Block target = b.getRelative(face); android.moveAndDig(b, inv, face, target); }), + /** + * This will make a {@link MinerAndroid} dig the {@link Block} below + * and then move itself to that new {@link Location}. + */ MOVE_AND_DIG_DOWN(AndroidType.MINER, HeadTexture.SCRIPT_DIG_DOWN, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.DOWN); android.moveAndDig(b, inv, face, target); }), - // Action - Sword + /** + * This will make a {@link ButcherAndroid} attack any {@link LivingEntity} + * ahead of them. + */ ATTACK_MOBS_ANIMALS(AndroidType.FIGHTER, HeadTexture.SCRIPT_ATTACK, (android, b, inv, face) -> { Predicate predicate = e -> true; android.attack(b, face, predicate); }), + /** + * This will make a {@link ButcherAndroid} attack any {@link Monster} + * ahead of them. + */ ATTACK_MOBS(AndroidType.FIGHTER, HeadTexture.SCRIPT_ATTACK, (android, b, inv, face) -> { Predicate predicate = e -> e instanceof Monster; android.attack(b, face, predicate); }), + /** + * This will make a {@link ButcherAndroid} attack any {@link Animals Animal} + * ahead of them. + */ ATTACK_ANIMALS(AndroidType.FIGHTER, HeadTexture.SCRIPT_ATTACK, (android, b, inv, face) -> { Predicate predicate = e -> e instanceof Animals; android.attack(b, face, predicate); }), + /** + * This will make a {@link ButcherAndroid} attack any adult + * {@link Animals Animal} ahead of them. + */ ATTACK_ANIMALS_ADULT(AndroidType.FIGHTER, HeadTexture.SCRIPT_ATTACK, (android, b, inv, face) -> { - Predicate predicate = e -> e instanceof Animals && e instanceof Ageable && ((Ageable) e).isAdult(); + Predicate predicate = e -> e instanceof Animals && ((Ageable) e).isAdult(); android.attack(b, face, predicate); }), - // Action - Axe + /** + * This will make a {@link WoodcutterAndroid} chop down the tree in front of them. + */ CHOP_TREE(AndroidType.WOODCUTTER, HeadTexture.SCRIPT_CHOP_TREE), - // Action - Fishing Rod + /** + * This {@link Instruction} makes a {@link FisherAndroid} try to catch fish from + * the water below. + */ CATCH_FISH(AndroidType.FISHERMAN, HeadTexture.SCRIPT_FISH, (android, b, inv, face) -> android.fish(b, inv)), - // Action - Hoe + /** + * This {@link Instruction} will make a {@link FarmerAndroid} try to harvest + * the {@link Block} in front of them. + */ FARM_FORWARD(AndroidType.FARMER, HeadTexture.SCRIPT_FARM_FORWARD, (android, b, inv, face) -> { Block target = b.getRelative(face); android.farm(inv, target); }), + /** + * This {@link Instruction} will make a {@link FarmerAndroid} try to harvest + * the {@link Block} below. + */ FARM_DOWN(AndroidType.FARMER, HeadTexture.SCRIPT_FARM_DOWN, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.DOWN); android.farm(inv, target); }), - // Action - ExoticGarden + /** + * This {@link Instruction} will make a {@link FarmerAndroid} try to harvest + * the {@link Block} in front of them. + * + * This includes plants from ExoticGarden. + */ FARM_EXOTIC_FORWARD(AndroidType.ADVANCED_FARMER, HeadTexture.SCRIPT_FARM_FORWARD, (android, b, inv, face) -> { Block target = b.getRelative(face); android.exoticFarm(inv, target); }), + /** + * This {@link Instruction} will make a {@link FarmerAndroid} try to harvest + * the {@link Block} below. + * + * This includes plants from ExoticGarden. + */ FARM_EXOTIC_DOWN(AndroidType.ADVANCED_FARMER, HeadTexture.SCRIPT_FARM_DOWN, (android, b, inv, face) -> { Block target = b.getRelative(BlockFace.DOWN); android.exoticFarm(inv, target); }), - // Action - Interface + /** + * This {@link Instruction} will force the {@link ProgrammableAndroid} to push their + * items into an {@link AndroidInterface} ahead of them. + */ INTERFACE_ITEMS(AndroidType.NONE, HeadTexture.SCRIPT_PUSH_ITEMS, (android, b, inv, face) -> { Block target = b.getRelative(face); android.depositItems(inv, target); }), + /** + * This {@link Instruction} will force the {@link ProgrammableAndroid} to pull + * fuel from an {@link AndroidInterface} ahead of them. + */ INTERFACE_FUEL(AndroidType.NONE, HeadTexture.SCRIPT_PULL_FUEL, (android, b, inv, face) -> { Block target = b.getRelative(face); android.refuel(inv, target); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java index b57b449e4..07d828f96 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/ProgrammableAndroid.java @@ -58,6 +58,7 @@ import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.interfaces.InventoryBlock; import me.mrCookieSlime.Slimefun.Objects.handlers.BlockTicker; import me.mrCookieSlime.Slimefun.Objects.handlers.ItemHandler; import me.mrCookieSlime.Slimefun.api.BlockStorage; +import me.mrCookieSlime.Slimefun.api.Slimefun; import me.mrCookieSlime.Slimefun.api.SlimefunItemStack; import me.mrCookieSlime.Slimefun.api.inventory.BlockMenu; import me.mrCookieSlime.Slimefun.api.inventory.BlockMenuPreset; @@ -69,6 +70,7 @@ public class ProgrammableAndroid extends SlimefunItem implements InventoryBlock, private static final int[] BORDER = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 18, 24, 25, 26, 27, 33, 35, 36, 42, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53 }; private static final int[] OUTPUT_BORDER = { 10, 11, 12, 13, 14, 19, 23, 28, 32, 37, 38, 39, 40, 41 }; private static final String DEFAULT_SCRIPT = "START-TURN_LEFT-REPEAT"; + private static final int MAX_SCRIPT_LENGTH = 54; protected final List fuelTypes = new ArrayList<>(); protected final String texture; @@ -407,19 +409,23 @@ public class ProgrammableAndroid extends SlimefunItem implements InventoryBlock, } else { Script script = scripts.get(target); menu.addItem(index, script.getAsItemStack(this, p), (player, slot, stack, action) -> { - if (action.isShiftClicked()) { - if (script.isAuthor(player)) { - SlimefunPlugin.getLocalization().sendMessage(player, "android.scripts.rating.own", true); - } else if (script.canRate(player)) { - script.rate(player, !action.isRightClicked()); - openScriptDownloader(player, b, page); - } else { - SlimefunPlugin.getLocalization().sendMessage(player, "android.scripts.rating.already", true); + try { + if (action.isShiftClicked()) { + if (script.isAuthor(player)) { + SlimefunPlugin.getLocalization().sendMessage(player, "android.scripts.rating.own", true); + } else if (script.canRate(player)) { + script.rate(player, !action.isRightClicked()); + openScriptDownloader(player, b, page); + } else { + SlimefunPlugin.getLocalization().sendMessage(player, "android.scripts.rating.already", true); + } + } else if (!action.isRightClicked()) { + script.download(); + setScript(b.getLocation(), script.getSourceCode()); + openScriptEditor(player, b); } - } else if (!action.isRightClicked()) { - script.download(); - setScript(b.getLocation(), script.getSourceCode()); - openScriptEditor(player, b); + } catch (Exception x) { + Slimefun.getLogger().log(Level.SEVERE, "An Exception was thrown when a User tried to download a Script!", x); } return false; @@ -534,12 +540,19 @@ public class ProgrammableAndroid extends SlimefunItem implements InventoryBlock, } @Nonnull - protected String getScript(@Nonnull Location l) { + public String getScript(@Nonnull Location l) { + Validate.notNull(l, "Location for android not specified"); String script = BlockStorage.getLocationInfo(l, "script"); return script != null ? script : DEFAULT_SCRIPT; } - protected void setScript(@Nonnull Location l, @Nonnull String script) { + public void setScript(@Nonnull Location l, @Nonnull String script) { + Validate.notNull(l, "Location for android not specified"); + Validate.notNull(script, "No script given"); + Validate.isTrue(script.startsWith(Instruction.START.name() + '-'), "A script must begin with a 'START' token."); + Validate.isTrue(script.endsWith('-' + Instruction.REPEAT.name()), "A script must end with a 'REPEAT' token."); + Validate.isTrue(PatternUtils.DASH.split(script).length <= MAX_SCRIPT_LENGTH, "Scripts may not have more than " + MAX_SCRIPT_LENGTH + " segments"); + BlockStorage.addBlockInfo(l, "script", script); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Script.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Script.java index 5d97975f7..7ee370638 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Script.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/Script.java @@ -130,18 +130,19 @@ public final class Script { @Nonnull ItemStack getAsItemStack(@Nonnull ProgrammableAndroid android, @Nonnull Player p) { List lore = new LinkedList<>(); - lore.add("&7by &r" + getAuthor()); + lore.add("&7by &f" + getAuthor()); lore.add(""); - lore.add("&7Downloads: &r" + getDownloads()); + lore.add("&7Downloads: &f" + getDownloads()); lore.add("&7Rating: " + getScriptRatingPercentage()); lore.add("&a" + getUpvotes() + " \u263A &7| &4\u2639 " + getDownvotes()); lore.add(""); - lore.add("&eLeft Click &rto download this Script"); + lore.add("&eLeft Click &fto download this Script"); lore.add("&4(This will override your current Script)"); if (canRate(p)) { - lore.add("&eShift + Left Click &rto leave a positive Rating"); - lore.add("&eShift + Right Click &rto leave a negative Rating"); + lore.add(""); + lore.add("&eShift + Left Click &fto leave a positive Rating"); + lore.add("&eShift + Right Click &fto leave a negative Rating"); } return new CustomItem(android.getItem(), "&b" + getName(), lore.toArray(new String[0])); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java index 1cb865b36..608a0e2f5 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java @@ -2,6 +2,8 @@ package io.github.thebusybiscuit.slimefun4.implementation.items.multiblocks; import java.util.List; +import javax.annotation.ParametersAreNonnullByDefault; + import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.block.Block; @@ -14,7 +16,6 @@ import org.bukkit.inventory.ItemStack; import io.github.thebusybiscuit.cscorelib2.inventory.ItemUtils; import io.github.thebusybiscuit.cscorelib2.item.CustomItem; -import io.github.thebusybiscuit.slimefun4.core.multiblocks.MultiBlockMachine; import io.github.thebusybiscuit.slimefun4.implementation.SlimefunPlugin; import io.github.thebusybiscuit.slimefun4.utils.SlimefunUtils; import io.papermc.lib.PaperLib; @@ -23,7 +24,7 @@ import me.mrCookieSlime.Slimefun.Objects.Category; import me.mrCookieSlime.Slimefun.api.Slimefun; import me.mrCookieSlime.Slimefun.api.SlimefunItemStack; -public class ArmorForge extends MultiBlockMachine { +public class ArmorForge extends BackpackCrafter { public ArmorForge(Category category, SlimefunItemStack item) { super(category, item, new ItemStack[] { null, null, null, null, new ItemStack(Material.ANVIL), null, null, new CustomItem(Material.DISPENSER, "Dispenser (Facing up)"), null }, BlockFace.SELF); @@ -31,8 +32,8 @@ public class ArmorForge extends MultiBlockMachine { @Override public void onInteract(Player p, Block b) { - Block dispBlock = b.getRelative(BlockFace.DOWN); - BlockState state = PaperLib.getBlockState(dispBlock, false).getState(); + Block dispenser = b.getRelative(BlockFace.DOWN); + BlockState state = PaperLib.getBlockState(dispenser, false).getState(); if (state instanceof Dispenser) { Dispenser disp = (Dispenser) state; @@ -44,13 +45,7 @@ public class ArmorForge extends MultiBlockMachine { ItemStack output = RecipeType.getRecipeOutputList(this, inputs.get(i)).clone(); if (Slimefun.hasUnlocked(p, output, true)) { - Inventory outputInv = findOutputInventory(output, dispBlock, inv); - - if (outputInv != null) { - craft(p, output, inv, outputInv); - } else { - SlimefunPlugin.getLocalization().sendMessage(p, "machines.full-inventory", true); - } + craft(p, output, inv, dispenser); } return; @@ -71,26 +66,35 @@ public class ArmorForge extends MultiBlockMachine { return true; } - private void craft(Player p, ItemStack output, Inventory inv, Inventory outputInv) { - for (int j = 0; j < 9; j++) { - ItemStack item = inv.getContents()[j]; + @ParametersAreNonnullByDefault + private void craft(Player p, ItemStack output, Inventory inv, Block dispenser) { + Inventory fakeInv = createVirtualInventory(inv); + Inventory outputInv = findOutputInventory(output, dispenser, inv, fakeInv); - if (item != null && item.getType() != Material.AIR) { - ItemUtils.consumeItem(item, true); - } - } + if (outputInv != null) { + for (int j = 0; j < 9; j++) { + ItemStack item = inv.getContents()[j]; - for (int j = 0; j < 4; j++) { - int current = j; - - SlimefunPlugin.runSync(() -> { - if (current < 3) { - p.getWorld().playSound(p.getLocation(), Sound.BLOCK_ANVIL_USE, 1F, 2F); - } else { - p.getWorld().playSound(p.getLocation(), Sound.ENTITY_ARROW_HIT_PLAYER, 1F, 1F); - outputInv.addItem(output); + if (item != null && item.getType() != Material.AIR) { + ItemUtils.consumeItem(item, true); } - }, j * 20L); + } + + for (int j = 0; j < 4; j++) { + int current = j; + + SlimefunPlugin.runSync(() -> { + if (current < 3) { + p.getWorld().playSound(p.getLocation(), Sound.BLOCK_ANVIL_USE, 1F, 2F); + } else { + p.getWorld().playSound(p.getLocation(), Sound.ENTITY_ARROW_HIT_PLAYER, 1F, 1F); + outputInv.addItem(output); + } + }, j * 20L); + } + + } else { + SlimefunPlugin.getLocalization().sendMessage(p, "machines.full-inventory", true); } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/BackpackCrafter.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/BackpackCrafter.java index 1a7615221..baf52187a 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/BackpackCrafter.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/BackpackCrafter.java @@ -32,6 +32,7 @@ import me.mrCookieSlime.Slimefun.api.SlimefunItemStack; * * @see EnhancedCraftingTable * @see MagicWorkbench + * @see ArmorForge * */ abstract class BackpackCrafter extends MultiBlockMachine { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunBootsListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunBootsListener.java index 8120b61c8..9d24ff2f5 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunBootsListener.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunBootsListener.java @@ -40,7 +40,7 @@ public class SlimefunBootsListener implements Listener { @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onDamage(EntityDamageEvent e) { if (e.getEntity() instanceof Player && e.getCause() == DamageCause.FALL) { - onFallDamage(e); + onFallDamage(e); } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java index 85092ae9a..87a2861f8 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/setup/SlimefunItemSetup.java @@ -206,7 +206,7 @@ import me.mrCookieSlime.Slimefun.api.SlimefunItemStack; * */ public final class SlimefunItemSetup { - + private static boolean registeredItems = false; private SlimefunItemSetup() {} @@ -218,7 +218,8 @@ public final class SlimefunItemSetup { registeredItems = true; DefaultCategories categories = new DefaultCategories(); - + + // @formatter:off (We will need to refactor this one day) new SlimefunItem(categories.weapons, SlimefunItems.GRANDMAS_WALKING_STICK, RecipeType.ENHANCED_CRAFTING_TABLE, new ItemStack[] {null, new ItemStack(Material.OAK_LOG), null, null, new ItemStack(Material.OAK_LOG), null, null, new ItemStack(Material.OAK_LOG), null}) .register(plugin); @@ -1092,7 +1093,7 @@ public final class SlimefunItemSetup { new RestoredBackpack(categories.usefulItems).register(plugin); - new SlimefunItem(categories.technicalComponents, SlimefunItems.MAGNET, RecipeType.SMELTERY, + new UnplaceableBlock(categories.technicalComponents, SlimefunItems.MAGNET, RecipeType.SMELTERY, new ItemStack[] {SlimefunItems.NICKEL_INGOT, SlimefunItems.ALUMINUM_DUST, SlimefunItems.IRON_DUST, SlimefunItems.COBALT_INGOT, null, null, null, null, null}) .register(plugin); @@ -1303,15 +1304,15 @@ public final class SlimefunItemSetup { new ItemStack[] {SlimefunItems.CARBONADO, SlimefunItems.BASIC_CIRCUIT_BOARD, SlimefunItems.CARBONADO, SlimefunItems.HEATING_COIL, SlimefunItems.REINFORCED_FURNACE, SlimefunItems.HEATING_COIL, SlimefunItems.CARBONADO, SlimefunItems.ELECTRIC_MOTOR, SlimefunItems.CARBONADO}) .register(plugin); - new SlimefunItem(categories.technicalComponents, SlimefunItems.ELECTRO_MAGNET, RecipeType.ENHANCED_CRAFTING_TABLE, + new UnplaceableBlock(categories.technicalComponents, SlimefunItems.ELECTRO_MAGNET, RecipeType.ENHANCED_CRAFTING_TABLE, new ItemStack[] {SlimefunItems.NICKEL_INGOT, SlimefunItems.MAGNET, SlimefunItems.COBALT_INGOT, null, SlimefunItems.BATTERY, null, null, null, null}) .register(plugin); - new SlimefunItem(categories.technicalComponents, SlimefunItems.ELECTRIC_MOTOR, RecipeType.ENHANCED_CRAFTING_TABLE, + new UnplaceableBlock(categories.technicalComponents, SlimefunItems.ELECTRIC_MOTOR, RecipeType.ENHANCED_CRAFTING_TABLE, new ItemStack[] {SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, null, SlimefunItems.ELECTRO_MAGNET, null, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE}) .register(plugin); - new SlimefunItem(categories.technicalComponents, SlimefunItems.HEATING_COIL, RecipeType.ENHANCED_CRAFTING_TABLE, + new UnplaceableBlock(categories.technicalComponents, SlimefunItems.HEATING_COIL, RecipeType.ENHANCED_CRAFTING_TABLE, new ItemStack[] {SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, SlimefunItems.ELECTRIC_MOTOR, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE, SlimefunItems.COPPER_WIRE}) .register(plugin); @@ -1455,7 +1456,7 @@ public final class SlimefunItemSetup { new SlimefunItemStack(SlimefunItems.HARDENED_GLASS, 16)) .register(plugin); - new SlimefunItem(categories.technicalComponents, SlimefunItems.COOLING_UNIT, RecipeType.ENHANCED_CRAFTING_TABLE, + new UnplaceableBlock(categories.technicalComponents, SlimefunItems.COOLING_UNIT, RecipeType.ENHANCED_CRAFTING_TABLE, new ItemStack[] {new ItemStack(Material.ICE), new ItemStack(Material.ICE), new ItemStack(Material.ICE), SlimefunItems.ALUMINUM_INGOT, SlimefunItems.ELECTRIC_MOTOR, SlimefunItems.ALUMINUM_INGOT, new ItemStack(Material.ICE), new ItemStack(Material.ICE), new ItemStack(Material.ICE)}) .register(plugin); @@ -2867,7 +2868,7 @@ public final class SlimefunItemSetup { }.register(plugin); - new SlimefunItem(categories.cargo, SlimefunItems.CARGO_MOTOR, RecipeType.ENHANCED_CRAFTING_TABLE, + new UnplaceableBlock(categories.cargo, SlimefunItems.CARGO_MOTOR, RecipeType.ENHANCED_CRAFTING_TABLE, new ItemStack[] {SlimefunItems.HARDENED_GLASS, SlimefunItems.ELECTRO_MAGNET, SlimefunItems.HARDENED_GLASS, SlimefunItems.SILVER_INGOT, SlimefunItems.ELECTRIC_MOTOR, SlimefunItems.SILVER_INGOT, SlimefunItems.HARDENED_GLASS, SlimefunItems.ELECTRO_MAGNET, SlimefunItems.HARDENED_GLASS}, new SlimefunItemStack(SlimefunItems.CARGO_MOTOR, 4)) .register(plugin); @@ -3048,6 +3049,8 @@ public final class SlimefunItemSetup { new ElytraCap(categories.magicalArmor, SlimefunItems.ELYTRA_CAP, RecipeType.ARMOR_FORGE, new ItemStack[]{new ItemStack(Material.SLIME_BALL), new ItemStack(Material.SLIME_BALL), new ItemStack(Material.SLIME_BALL), SlimefunItems.ELYTRA_SCALE, SlimefunItems.ELYTRA_SCALE, SlimefunItems.ELYTRA_SCALE, new ItemStack(Material.SLIME_BALL), new ItemStack(Material.LEATHER_HELMET), new ItemStack(Material.SLIME_BALL)}) .register(plugin); + + // @formatter:on } private static void registerArmorSet(Category category, ItemStack baseComponent, ItemStack[] items, String idSyntax, boolean vanilla, PotionEffect[][] effects, SlimefunAddon addon) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChestMenuUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChestMenuUtils.java index 7f566db35..0cb2ea26f 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChestMenuUtils.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChestMenuUtils.java @@ -29,6 +29,9 @@ public final class ChestMenuUtils { private static final ItemStack INPUT_SLOT = new SlimefunItemStack("_UI_INPUT_SLOT", Material.CYAN_STAINED_GLASS_PANE, " "); private static final ItemStack OUTPUT_SLOT = new SlimefunItemStack("_UI_OUTPUT_SLOT", Material.ORANGE_STAINED_GLASS_PANE, " "); + private static final ItemStack NO_PERMISSION = new SlimefunItemStack("_UI_NO_PERMISSION", Material.BARRIER, "No Permission"); + private static final ItemStack NOT_RESEARCHED = new SlimefunItemStack("_UI_NOT_RESEARCHED", Material.BARRIER, "Not researched"); + private static final ItemStack BACK_BUTTON = new SlimefunItemStack("_UI_BACK", Material.ENCHANTED_BOOK, "&7\u21E6 Back", meta -> meta.addItemFlags(ItemFlag.HIDE_ENCHANTS)); private static final ItemStack MENU_BUTTON = new SlimefunItemStack("_UI_MENU", Material.COMPARATOR, "&eSettings / Info", "", "&7\u21E8 Click to see more"); private static final ItemStack SEARCH_BUTTON = new SlimefunItemStack("_UI_SEARCH", Material.NAME_TAG, "&bSearch"); @@ -46,6 +49,16 @@ public final class ChestMenuUtils { return UI_BACKGROUND; } + @Nonnull + public static ItemStack getNoPermissionItem() { + return NO_PERMISSION; + } + + @Nonnull + public static ItemStack getNotResearchedItem() { + return NOT_RESEARCHED; + } + @Nonnull public static ItemStack getInputSlotTexture() { return INPUT_SLOT; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ColoredMaterials.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ColoredMaterials.java index 274f0ed09..7b4f8de31 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ColoredMaterials.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ColoredMaterials.java @@ -4,6 +4,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.annotation.Nonnull; + +import org.apache.commons.lang.Validate; import org.bukkit.Material; import io.github.thebusybiscuit.slimefun4.utils.tags.SlimefunTag; @@ -24,11 +27,13 @@ public final class ColoredMaterials { * constructor to be private. */ private ColoredMaterials() {} + + // @formatter:off (We want this to stay formatted like this) /** * This {@link List} contains all wool colors ordered by their appearance ingame. */ - public static final List WOOL = Collections.unmodifiableList(Arrays.asList( + public static final List WOOL = asList(new Material[] { Material.WHITE_WOOL, Material.ORANGE_WOOL, Material.MAGENTA_WOOL, @@ -45,12 +50,12 @@ public final class ColoredMaterials { Material.GREEN_WOOL, Material.RED_WOOL, Material.BLACK_WOOL - )); + }); /** * This {@link List} contains all stained glass colors ordered by their appearance ingame. */ - public static final List STAINED_GLASS = Collections.unmodifiableList(Arrays.asList( + public static final List STAINED_GLASS = asList(new Material[] { Material.WHITE_STAINED_GLASS, Material.ORANGE_STAINED_GLASS, Material.MAGENTA_STAINED_GLASS, @@ -67,12 +72,12 @@ public final class ColoredMaterials { Material.GREEN_STAINED_GLASS, Material.RED_STAINED_GLASS, Material.BLACK_STAINED_GLASS - )); + }); /** * This {@link List} contains all stained glass pane colors ordered by their appearance ingame. */ - public static final List STAINED_GLASS_PANE = Collections.unmodifiableList(Arrays.asList( + public static final List STAINED_GLASS_PANE = asList(new Material[] { Material.WHITE_STAINED_GLASS_PANE, Material.ORANGE_STAINED_GLASS_PANE, Material.MAGENTA_STAINED_GLASS_PANE, @@ -89,12 +94,12 @@ public final class ColoredMaterials { Material.GREEN_STAINED_GLASS_PANE, Material.RED_STAINED_GLASS_PANE, Material.BLACK_STAINED_GLASS_PANE - )); + }); /** * This {@link List} contains all terracotta colors ordered by their appearance ingame. */ - public static final List TERRACOTTA = Collections.unmodifiableList(Arrays.asList( + public static final List TERRACOTTA = asList(new Material[] { Material.WHITE_TERRACOTTA, Material.ORANGE_TERRACOTTA, Material.MAGENTA_TERRACOTTA, @@ -111,12 +116,12 @@ public final class ColoredMaterials { Material.GREEN_TERRACOTTA, Material.RED_TERRACOTTA, Material.BLACK_TERRACOTTA - )); + }); /** * This {@link List} contains all glazed terracotta colors ordered by their appearance ingame. */ - public static final List GLAZED_TERRACOTTA = Collections.unmodifiableList(Arrays.asList( + public static final List GLAZED_TERRACOTTA = asList(new Material[] { Material.WHITE_GLAZED_TERRACOTTA, Material.ORANGE_GLAZED_TERRACOTTA, Material.MAGENTA_GLAZED_TERRACOTTA, @@ -133,12 +138,12 @@ public final class ColoredMaterials { Material.GREEN_GLAZED_TERRACOTTA, Material.RED_GLAZED_TERRACOTTA, Material.BLACK_GLAZED_TERRACOTTA - )); + }); /** * This {@link List} contains all concrete colors ordered by their appearance ingame. */ - public static final List CONCRETE = Collections.unmodifiableList(Arrays.asList( + public static final List CONCRETE = asList(new Material[] { Material.WHITE_CONCRETE, Material.ORANGE_CONCRETE, Material.MAGENTA_CONCRETE, @@ -155,6 +160,16 @@ public final class ColoredMaterials { Material.GREEN_CONCRETE, Material.RED_CONCRETE, Material.BLACK_CONCRETE - )); + }); + + // @formatter:on + + @Nonnull + private static List asList(@Nonnull Material[] materials) { + Validate.noNullElements(materials, "The List cannot contain any null elements"); + Validate.isTrue(materials.length == 16, "Expected 16, received: " + materials.length + ". Did you miss a color?"); + + return Collections.unmodifiableList(Arrays.asList(materials)); + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/NumberUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/NumberUtils.java index 8dd69f881..d752b5ec8 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/NumberUtils.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/NumberUtils.java @@ -1,6 +1,7 @@ package io.github.thebusybiscuit.slimefun4.utils; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.time.Duration; import java.time.LocalDateTime; @@ -24,7 +25,7 @@ public final class NumberUtils { /** * This is our {@link DecimalFormat} for decimal values. */ - private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##"); + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##", DecimalFormatSymbols.getInstance(Locale.ROOT)); /** * We do not want any instance of this to be created. @@ -105,8 +106,31 @@ public final class NumberUtils { */ @Nonnull public static String getElapsedTime(@Nonnull LocalDateTime date) { - Validate.notNull(date, "Provided date was null"); - long hours = Duration.between(date, LocalDateTime.now()).toHours(); + return getElapsedTime(LocalDateTime.now(), date); + } + + /** + * This returns the elapsed time between the two given {@link LocalDateTime LocalDateTimes}. + * The output will be nicely formatted based on the elapsed hours or days between the + * given {@link LocalDateTime LocalDateTime}. + * + * If a {@link LocalDateTime} from today and yesterday (exactly 24h apart) was passed it + * will return {@code "1d"}. + * One hour later it will read {@code "1d 1h"}. For values smaller than an hour {@code "< 1h"} + * will be returned instead. + * + * @param start + * The starting {@link LocalDateTime}. + * @param end + * The ending {@link LocalDateTime}. + * + * @return The elapsed time as a {@link String} + */ + @Nonnull + public static String getElapsedTime(@Nonnull LocalDateTime start, @Nonnull LocalDateTime end) { + Validate.notNull(start, "Provided start was null"); + Validate.notNull(end, "Provided end was null"); + long hours = Duration.between(start, end).toHours(); if (hours == 0) { return "< 1h"; @@ -133,12 +157,24 @@ public final class NumberUtils { return timeleft + seconds + "s"; } + /** + * This method parses a {@link String} into an {@link Integer}. + * If the {@link String} could not be parsed correctly, the provided + * default value will be returned instead. + * + * @param str + * The {@link String} to parse + * @param defaultValue + * The default value for when the {@link String} could not be parsed + * + * @return The resulting {@link Integer} + */ public static int getInt(@Nonnull String str, int defaultValue) { if (PatternUtils.NUMERIC.matcher(str).matches()) { return Integer.parseInt(str); + } else { + return defaultValue; } - - return defaultValue; } @Nonnull @@ -183,6 +219,8 @@ public final class NumberUtils { * The value to clamp * @param max * The maximum value + * + * @return The clamped value */ public static int clamp(int min, int value, int max) { if (value < min) { diff --git a/src/main/java/me/mrCookieSlime/Slimefun/Objects/SlimefunItem/abstractItems/AContainer.java b/src/main/java/me/mrCookieSlime/Slimefun/Objects/SlimefunItem/abstractItems/AContainer.java index daf180de3..b6e36dcd8 100644 --- a/src/main/java/me/mrCookieSlime/Slimefun/Objects/SlimefunItem/abstractItems/AContainer.java +++ b/src/main/java/me/mrCookieSlime/Slimefun/Objects/SlimefunItem/abstractItems/AContainer.java @@ -5,9 +5,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -238,31 +241,30 @@ public abstract class AContainer extends SlimefunItem implements InventoryBlock, BlockMenu inv = BlockStorage.getInventory(b); if (isProcessing(b)) { - int timeleft = progress.get(b); - if (timeleft > 0) { - ChestMenuUtils.updateProgressbar(inv, 22, timeleft, processing.get(b).getTicks(), getProgressBar()); + if (takeCharge(b.getLocation())) { - if (isChargeable()) { - if (getCharge(b.getLocation()) < getEnergyConsumption()) { - return; + int timeleft = progress.get(b); + + if (timeleft > 0) { + ChestMenuUtils.updateProgressbar(inv, 22, timeleft, processing.get(b).getTicks(), getProgressBar()); + + progress.put(b, timeleft - 1); + } else { + + inv.replaceExistingItem(22, new CustomItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + + for (ItemStack output : processing.get(b).getOutput()) { + inv.pushItem(output.clone(), getOutputSlots()); } - removeCharge(b.getLocation(), getEnergyConsumption()); + Bukkit.getPluginManager().callEvent(new AsyncMachineProcessCompleteEvent(b.getLocation(), AContainer.this, getProcessing(b))); + + progress.remove(b); + processing.remove(b); } - progress.put(b, timeleft - 1); - } else { - inv.replaceExistingItem(22, new CustomItem(Material.BLACK_STAINED_GLASS_PANE, " ")); - - for (ItemStack output : processing.get(b).getOutput()) { - inv.pushItem(output.clone(), getOutputSlots()); - } - - Bukkit.getPluginManager().callEvent(new AsyncMachineProcessCompleteEvent(b.getLocation(), AContainer.this, getProcessing(b))); - - progress.remove(b); - processing.remove(b); } + } else { MachineRecipe next = findNextRecipe(inv); @@ -273,6 +275,26 @@ public abstract class AContainer extends SlimefunItem implements InventoryBlock, } } + /** + * This method will remove charge from a location if it is chargeable. + * + * @param l + * location to try to remove charge from + * @return Whether charge was taken if its chargeable + */ + protected boolean takeCharge(@Nonnull Location l) { + Validate.notNull(l, "Can't attempt to take charge from a null location!"); + + if (isChargeable()) { + if (getCharge(l) < getEnergyConsumption()) { + return false; + } + + removeCharge(l, getEnergyConsumption()); + } + return true; + } + protected MachineRecipe findNextRecipe(BlockMenu inv) { Map inventory = new HashMap<>(); diff --git a/src/main/resources/languages/messages_hu.yml b/src/main/resources/languages/messages_hu.yml index 8320c757b..5bfa56bdd 100644 --- a/src/main/resources/languages/messages_hu.yml +++ b/src/main/resources/languages/messages_hu.yml @@ -244,6 +244,7 @@ machines: finished: Az Industrial Miner-ed kész! Összesen %ores% ércet szerzett! anvil: not-working: "&4Nem használhatsz Slimefun tárgyakat az üllőben!" + mcmmo-salvaging: "&4Nem hasznosíthatsz újra Slimefun tárgyakat!" backpack: already-open: "&cSajnáljuk, ez a hátizsák valahol máshol már nyitva van!" no-stack: "&cNem halmozhatsz hátizsákokat" @@ -306,7 +307,7 @@ android: rating: own: "&4Nem értékelheted a saját szkriptedet!" already: "&4Ezt a szkriptet már értékelted!" - editor: Szkript Szerkesztő + editor: Szkript szerkesztő languages: default: Szerver-alapértelmezett en: Angol @@ -328,7 +329,6 @@ languages: zh-CN: Kínai (Kína) el: Görög he: Héber - pt-BR: Portugál (Brazília) ar: Arab af: Afrikaans da: Dán @@ -341,6 +341,7 @@ languages: th: Thai ro: Román pt: Portugál (Portugália) + pt-BR: Portugál (Brazília) bg: Bolgár ko: Koreai tr: Török @@ -355,5 +356,7 @@ villagers: no-trading: "&4Nem cserélhetsz Slimefun tárgyakat falusiakkal!" cartography_table: not-working: "&4Nem használhatsz Slimefun tárgyakat térképasztalban." +cauldron: + no-discoloring: "&4Nem színteleníthetsz Slimefun páncélt!" miner: no-ores: "&eSajnálom, nem találtam semmilyen ércet a közelben!" diff --git a/src/main/resources/languages/researches_hu.yml b/src/main/resources/languages/researches_hu.yml index 2715e2184..f1bf8cedc 100644 --- a/src/main/resources/languages/researches_hu.yml +++ b/src/main/resources/languages/researches_hu.yml @@ -246,3 +246,4 @@ slimefun: caveman_talisman: Az Ősember talizmánja even_higher_tier_capacitors: 3. szintű kondenzátorok elytra_cap: Ütközésvédelem + energy_connectors: Vezetékes csatlakozás diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/testing/tests/utils/TestNumberUtils.java b/src/test/java/io/github/thebusybiscuit/slimefun4/testing/tests/utils/TestNumberUtils.java new file mode 100644 index 000000000..6b33de558 --- /dev/null +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/testing/tests/utils/TestNumberUtils.java @@ -0,0 +1,78 @@ +package io.github.thebusybiscuit.slimefun4.testing.tests.utils; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.github.thebusybiscuit.slimefun4.utils.NumberUtils; + +class TestNumberUtils { + + @Test + @DisplayName("Test NumberUtils.clamp(...)") + void testHumanize() { + // Below minimum + Assertions.assertEquals(2, NumberUtils.clamp(2, 0, 5)); + + // Normal + Assertions.assertEquals(3, NumberUtils.clamp(0, 3, 5)); + + // Above maximum + Assertions.assertEquals(20, NumberUtils.clamp(1, 100, 20)); + } + + @Test + @DisplayName("Test elapsed time string") + void testElapsedTime() { + LocalDateTime start = LocalDateTime.now(); + + LocalDateTime a = start.plusDays(1); + Assertions.assertEquals("1d", NumberUtils.getElapsedTime(start, a)); + + LocalDateTime b = start.plusHours(25); + Assertions.assertEquals("1d 1h", NumberUtils.getElapsedTime(start, b)); + + LocalDateTime c = start.plusHours(1); + Assertions.assertEquals("1h", NumberUtils.getElapsedTime(start, c)); + + LocalDateTime d = start.plusMinutes(12); + Assertions.assertEquals("< 1h", NumberUtils.getElapsedTime(start, d)); + } + + @Test + @DisplayName("Test Integer parsing") + void testIntegerParsing() { + Assertions.assertEquals(6, NumberUtils.getInt("6", 0)); + Assertions.assertEquals(12, NumberUtils.getInt("I am a String", 12)); + } + + @Test + @DisplayName("Test nullable Long") + void testNullableLong() { + Assertions.assertEquals(10, NumberUtils.getLong(10L, 20L)); + Assertions.assertEquals(20, NumberUtils.getLong(null, 20L)); + } + + @Test + @DisplayName("Test nullable Int") + void testNullableInt() { + Assertions.assertEquals(10, NumberUtils.getInt(10, 20)); + Assertions.assertEquals(20, NumberUtils.getInt((Integer) null, 20)); + } + + @Test + @DisplayName("Test nullable Float") + void testNullableFloat() { + Assertions.assertEquals(10, NumberUtils.getFloat(10F, 20F)); + Assertions.assertEquals(20, NumberUtils.getFloat(null, 20F)); + } + + @Test + @DisplayName("Test decimal rounding") + void testRounding() { + Assertions.assertEquals("5.25", NumberUtils.roundDecimalNumber(5.249999999999)); + } + +}