diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..bdf2dd7 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,170 @@ + + +--- +name: desloppify +description: > + Codebase health scanner and technical debt tracker. Use when the user asks + about code quality, technical debt, dead code, large files, god classes, + duplicate functions, code smells, naming issues, import cycles, or coupling + problems. Also use when asked for a health score, what to fix next, or to + create a cleanup plan. Supports 28 languages. +allowed-tools: Bash(desloppify *) +--- + +# Desloppify + +## 1. Your Job + +Improve code quality by maximising the **strict score** honestly. + +**The main thing you do is run `desloppify next`** — it tells you exactly what to fix and how. Fix it, resolve it, run `next` again. Keep going. + +Follow the scan output's **INSTRUCTIONS FOR AGENTS** — don't substitute your own analysis. + +## 2. The Workflow + +Two loops. The **outer loop** rescans periodically to measure progress. +The **inner loop** is where you spend most of your time: fixing issues one by one. + +### Outer loop — scan and check + +```bash +desloppify scan --path . # analyse the codebase +desloppify status # check scores — are we at target? +``` +If not at target, work the inner loop. Rescan periodically — especially after clearing a cluster or batch of related fixes. Issues cascade-resolve and new ones may surface. + +### Inner loop — fix issues + +Repeat until the queue is clear: + +``` +1. desloppify next ← tells you exactly what to fix next +2. Fix the issue in code +3. Resolve it (next shows you the exact command including required attestation) +``` + +Score may temporarily drop after fixes — cascade effects are normal, keep going. +If `next` suggests an auto-fixer, run `desloppify fix --dry-run` to preview, then apply. + +**To be strategic**, use `plan` to shape what `next` gives you: +```bash +desloppify plan # see the full ordered queue +desloppify plan move top # reorder — what unblocks the most? +desloppify plan cluster create # group related issues to batch-fix +desloppify plan focus # scope next to one cluster +desloppify plan defer # push low-value items aside +desloppify plan skip # hide from next +desloppify plan done # mark complete +desloppify plan reopen # reopen +``` + +### Subjective reviews + +The scan will prompt you when a subjective review is needed — just follow its instructions. +If you need to trigger one manually: +```bash +desloppify review --run-batches --runner codex --parallel --scan-after-import +``` + +### Other useful commands + +```bash +desloppify next --count 5 # top 5 priorities +desloppify next --cluster # drill into a cluster +desloppify show # filter by file/detector/ID +desloppify show --status open # all open findings +desloppify plan skip --permanent "" --note "reason" # accept debt (lowers strict score) +desloppify scan --path . --reset-subjective # reset subjective baseline to 0 +``` + +## 3. Reference + +### How scoring works + +Overall score = **40% mechanical** + **60% subjective**. + +- **Mechanical (40%)**: auto-detected issues — duplication, dead code, smells, unused imports, security. Fixed by changing code and rescanning. +- **Subjective (60%)**: design quality review — naming, error handling, abstractions, clarity. Starts at **0%** until reviewed. The scan will prompt you when a review is needed. +- **Strict score** is the north star: wontfix items count as open. The gap between overall and strict is your wontfix debt. +- **Score types**: overall (lenient), strict (wontfix counts), objective (mechanical only), verified (confirmed fixes only). + +### Subjective reviews in detail + +- **Preferred**: `desloppify review --run-batches --runner codex --parallel --scan-after-import` — does everything in one command. +- **Manual path**: `desloppify review --prepare` → review per dimension → `desloppify review --import file.json`. +- Import first, fix after — import creates tracked state entries for correlation. +- Integrity: reviewers score from evidence only. Scores hitting exact targets trigger auto-reset. +- Even moderate scores (60-80) dramatically improve overall health. +- Stale dimensions auto-surface in `next` — just follow the queue. + +### Key concepts + +- **Tiers**: T1 auto-fix → T2 quick manual → T3 judgment call → T4 major refactor. +- **Auto-clusters**: related findings are auto-grouped in `next`. Drill in with `next --cluster `. +- **Zones**: production/script (scored), test/config/generated/vendor (not scored). Fix with `zone set`. +- **Wontfix cost**: widens the lenient↔strict gap. Challenge past decisions when the gap grows. +- Score can temporarily drop after fixes (cascade effects are normal). + +## 4. Escalate Tool Issues Upstream + +When desloppify itself appears wrong or inconsistent: + +1. Capture a minimal repro (`command`, `path`, `expected`, `actual`). +2. Open a GitHub issue in `peteromallet/desloppify`. +3. If you can fix it safely, open a PR linked to that issue. +4. If unsure whether it is tool bug vs user workflow, issue first, PR second. + +## Prerequisite + +`command -v desloppify >/dev/null 2>&1 && echo "desloppify: installed" || echo "NOT INSTALLED — run: pip install --upgrade git+https://github.com/peteromallet/desloppify.git"` + + + +## VS Code Copilot Overlay + +VS Code Copilot supports native subagents via `.github/agents/` definitions. +Use them for context-isolated subjective reviews. + +### Subjective review + +1. **Preferred**: `desloppify review --run-batches --runner codex --parallel --scan-after-import`. +2. **Copilot/cloud path**: `desloppify review --external-start --external-runner claude` → use generated prompt/template → run printed `--external-submit` command. +3. **Manual path**: define a reviewer agent, split dimensions, merge, import. + +For the manual path, define a reviewer in `.github/agents/desloppify-reviewer.md`: + +```yaml +--- +name: desloppify-reviewer +tools: ['read', 'search'] +--- +You are a code quality reviewer. You will be given a codebase path, a set of +dimensions to score, and what each dimension means. Read the code, score each +dimension 0-100 from evidence only, and return JSON in the required format. +Do not anchor to target thresholds. When evidence is mixed, score lower and +explain uncertainty. +``` + +And an orchestrator in `.github/agents/desloppify-review-orchestrator.md`: + +```yaml +--- +name: desloppify-review-orchestrator +tools: ['agent', 'read', 'search'] +agents: ['desloppify-reviewer'] +--- +``` + +Split dimensions across `desloppify-reviewer` calls (Copilot runs them concurrently), merge assessments (average overlaps) and findings, then import. + +### Review integrity + +1. Do not use prior chat context, score history, or target-threshold anchoring while scoring. +2. Score from evidence only; when mixed, score lower and explain uncertainty. +3. Return JSON matching the format in the base skill doc. For `--external-submit`, include `session` from the generated template. +4. `findings` MUST match `query.system_prompt` exactly. Use `"findings": []` when no defects found. +5. Import is fail-closed: invalid findings abort unless `--allow-partial` is passed. + + + diff --git a/.gitignore b/.gitignore index bf3e1b2..10c5e64 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ bin/ ### Mac OS ### .DS_Store /.idea/ +/.desloppify diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 3660329..7a124b1 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -39,6 +39,11 @@ dependencies { // Lombok compileOnly("org.projectlombok:lombok:1.18.32") annotationProcessor("org.projectlombok:lombok:1.18.32") + + // Testing + testImplementation(platform("org.junit:junit-bom:5.12.2")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } java { @@ -53,6 +58,10 @@ tasks.shadowJar { archiveClassifier.set("") } +tasks.test { + useJUnitPlatform() +} + publishing { publications { create("mavenJava") { @@ -85,4 +94,4 @@ publishing { } } } -} \ No newline at end of file +} diff --git a/common/src/main/java/games/negative/engine/util/NumberUtil.java b/common/src/main/java/games/negative/engine/util/NumberUtil.java index 0234e09..d6999b6 100644 --- a/common/src/main/java/games/negative/engine/util/NumberUtil.java +++ b/common/src/main/java/games/negative/engine/util/NumberUtil.java @@ -391,18 +391,19 @@ public static String condense(BigDecimal number) { */ public static String condense(BigDecimal number, final char[] set) { BigDecimal thousand = BigDecimal.valueOf(1000); + String condensed; if (number.compareTo(thousand) < 0) { - return number.stripTrailingZeros().toPlainString(); // Return the number itself if less than 1000. - } - - int exp = (int) (Math.floor(Math.log10(number.doubleValue()) / 3)); - - String suffixes = (set == null) ? SUFFIXES : new String(set); - char suffix = suffixes.charAt(Math.min(exp - 1, suffixes.length() - 1)); + condensed = number.stripTrailingZeros().toPlainString(); // Return the number itself if less than 1000. + } else { + int exp = (int) (Math.floor(Math.log10(number.doubleValue()) / 3)); - BigDecimal result = number.divide(thousand.pow(exp), 1, RoundingMode.HALF_UP); + String suffixes = (set == null) ? SUFFIXES : new String(set); + char suffix = suffixes.charAt(Math.min(exp - 1, suffixes.length() - 1)); - return String.format("%.1f%c", result, suffix); + BigDecimal result = number.divide(thousand.pow(exp), 1, RoundingMode.HALF_UP); + condensed = String.format("%.1f%c", result, suffix); + } + return condensed; } /** diff --git a/common/src/main/java/games/negative/engine/util/TimeUtil.java b/common/src/main/java/games/negative/engine/util/TimeUtil.java index 6ff1ffb..c8db7c7 100644 --- a/common/src/main/java/games/negative/engine/util/TimeUtil.java +++ b/common/src/main/java/games/negative/engine/util/TimeUtil.java @@ -256,7 +256,7 @@ private static Duration parseUnitFormat(String input) { if (Character.isDigit(c)) { numberBuffer.append(c); } else if (Character.isWhitespace(c)) { - continue; // Allow whitespace + // Allow whitespace. } else { if (numberBuffer.isEmpty()) { throw new IllegalArgumentException( diff --git a/common/src/test/java/games/negative/engine/util/IntListTest.java b/common/src/test/java/games/negative/engine/util/IntListTest.java new file mode 100644 index 0000000..827762b --- /dev/null +++ b/common/src/test/java/games/negative/engine/util/IntListTest.java @@ -0,0 +1,18 @@ +package games.negative.engine.util; + +import games.negative.engine.util.IntList; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class IntListTest { + + @Test + void parseSupportsSinglesAndRanges() { + List values = IntList.parse(List.of("1", "3-5", " 7 ")); + + assertEquals(List.of(1, 3, 4, 5, 7), values); + } +} diff --git a/common/src/test/java/games/negative/engine/util/NumberUtilTest.java b/common/src/test/java/games/negative/engine/util/NumberUtilTest.java new file mode 100644 index 0000000..9bfc449 --- /dev/null +++ b/common/src/test/java/games/negative/engine/util/NumberUtilTest.java @@ -0,0 +1,24 @@ +package games.negative.engine.util; + +import games.negative.engine.util.NumberUtil; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NumberUtilTest { + + @Test + void fancyAddsExpectedSuffixes() { + assertEquals("1st", NumberUtil.fancy(1)); + assertEquals("12th", NumberUtil.fancy(12)); + assertEquals("23rd", NumberUtil.fancy(23)); + } + + @Test + void condenseFormatsValues() { + assertEquals("1.5k", NumberUtil.condense(1500)); + assertEquals("999", NumberUtil.condense(new BigDecimal("999"))); + } +} diff --git a/common/src/test/java/games/negative/engine/util/OptionalBoolTest.java b/common/src/test/java/games/negative/engine/util/OptionalBoolTest.java new file mode 100644 index 0000000..f6f5585 --- /dev/null +++ b/common/src/test/java/games/negative/engine/util/OptionalBoolTest.java @@ -0,0 +1,42 @@ +package games.negative.engine.util; + +import games.negative.engine.util.OptionalBool; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OptionalBoolTest { + + @Test + void ofReturnsSingletonInstances() { + assertSame(OptionalBool.of(true), OptionalBool.of(true)); + assertSame(OptionalBool.of(false), OptionalBool.of(false)); + } + + @Test + void mapIfTrueAndFalseBehaveAsExpected() { + Optional whenTrue = OptionalBool.of(true).mapIfTrue(() -> "value"); + Optional whenFalse = OptionalBool.of(false).mapIfTrue(() -> "value"); + + assertEquals(Optional.of("value"), whenTrue); + assertTrue(whenFalse.isEmpty()); + } + + @Test + void ifTrueOrElseRunsCorrectBranch() { + AtomicInteger marker = new AtomicInteger(0); + OptionalBool.of(false).ifTrueOrElse( + () -> marker.set(1), + () -> marker.set(2) + ); + + assertFalse(OptionalBool.of(false).isTrue()); + assertEquals(2, marker.get()); + } +} diff --git a/common/src/test/java/games/negative/engine/util/TimeUtilTest.java b/common/src/test/java/games/negative/engine/util/TimeUtilTest.java new file mode 100644 index 0000000..f0a6bf1 --- /dev/null +++ b/common/src/test/java/games/negative/engine/util/TimeUtilTest.java @@ -0,0 +1,26 @@ +package games.negative.engine.util; + +import games.negative.engine.util.TimeUtil; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TimeUtilTest { + + @Test + void formatAndParseRoundTripUnitFormat() { + Duration duration = TimeUtil.parse("1h30m"); + assertEquals(Duration.ofMinutes(90), duration); + assertEquals("1h 30m", TimeUtil.format(duration, true)); + } + + @Test + void formatAndParseRoundTripColonFormat() { + Duration duration = Duration.ofHours(1).plusMinutes(5).plusSeconds(2); + String formatted = TimeUtil.formatColonSeparated(duration); + assertEquals("1:05:02", formatted); + assertEquals(duration, TimeUtil.parseColonSeparated(formatted)); + } +} diff --git a/paper/src/main/java/games/negative/engine/paper/gui/ChestMenu.java b/paper/src/main/java/games/negative/engine/paper/gui/ChestMenu.java index 1c85cdc..99b3dae 100644 --- a/paper/src/main/java/games/negative/engine/paper/gui/ChestMenu.java +++ b/paper/src/main/java/games/negative/engine/paper/gui/ChestMenu.java @@ -3,11 +3,10 @@ import com.google.common.base.Preconditions; import games.negative.engine.message.util.MiniMessageUtil; import games.negative.engine.paper.gui.button.Button; +import games.negative.engine.paper.gui.util.MenuInteractionUtil; import games.negative.engine.paper.gui.util.SafeUtil; -import games.negative.engine.paper.scheduler.Scheduler; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; -import org.bukkit.event.Event; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryOpenEvent; @@ -109,21 +108,7 @@ public void onClose(Player player, InventoryCloseEvent event) { @Override public void onClick(Player player, InventoryClickEvent event) { - if (cancelClicks) { - event.setCancelled(true); - event.setResult(Event.Result.DENY); - } - - ItemStack current = event.getCurrentItem(); - if (current == null) return; - - String id = current.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); - if (id == null) return; - - Button button = buttonById.get(UUID.fromString(id)); - if (button == null) return; - - button.processClickAction(player, event); + MenuInteractionUtil.processClick(cancelClicks, buttonById, player, event); } /** @@ -131,13 +116,7 @@ public void onClick(Player player, InventoryClickEvent event) { */ @Override public void open() { - refresh(); - - UserInterface.invalidateFromCache(player.getUniqueId()); - - CACHE.put(player.getUniqueId(), this); - - Scheduler.entity(player).execute(() -> inventory.open(), 1); + MenuInteractionUtil.openMenu(player, this, this::refresh, () -> inventory); } /** @@ -189,18 +168,7 @@ public Collection> getRefreshingButtons() { * @param button The button to refresh */ public void refreshButton(int slot, Button button) { - if (inventory == null) return; - - Player player = (Player) inventory.getPlayer(); - - ItemStack stack = button.item(player); - if (stack == null || stack.getType().isAir()) return; - - stack.editPersistentDataContainer( - data -> data.set(Button.KEY, PersistentDataType.STRING, button.uuid().toString()) - ); - - SafeUtil.setInventoryItem(inventory, slot, stack); + MenuInteractionUtil.refreshButton(inventory, slot, button); } /** @@ -214,12 +182,7 @@ public void addButton(int slot, Button button) { "Slot must be between 0 and " + (rows * 9 - 1) ); - buttons.put(slot, button); - buttonById.put(button.uuid(), button); - - if (button.refreshIntervalTicks() <= 0L) return; - - refreshingButtons.put(slot, button); + MenuInteractionUtil.addButton(slot, button, buttons, buttonById, refreshingButtons); } /** @@ -320,16 +283,7 @@ public boolean cancelClicks() { * @return true if the click should be cancelled */ public boolean checkCancelClick(int slot) { - ItemStack item = inventory.getItem(slot); - if (item == null || item.getType().isAir()) return false; - - String id = item.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); - if (id == null) return false; - - Button button = buttonById.get(UUID.fromString(id)); - if (button == null) return false; - - return button.cancelClick(); + return MenuInteractionUtil.checkCancelClick(inventory, buttonById, slot); } } diff --git a/paper/src/main/java/games/negative/engine/paper/gui/HopperMenu.java b/paper/src/main/java/games/negative/engine/paper/gui/HopperMenu.java index 6e20160..5985480 100644 --- a/paper/src/main/java/games/negative/engine/paper/gui/HopperMenu.java +++ b/paper/src/main/java/games/negative/engine/paper/gui/HopperMenu.java @@ -2,11 +2,10 @@ import games.negative.engine.message.util.MiniMessageUtil; import games.negative.engine.paper.gui.button.Button; +import games.negative.engine.paper.gui.util.MenuInteractionUtil; import games.negative.engine.paper.gui.util.SafeUtil; -import games.negative.engine.paper.scheduler.Scheduler; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; -import org.bukkit.event.Event; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryOpenEvent; @@ -93,21 +92,7 @@ public void onClose(Player player, InventoryCloseEvent event) { @Override public void onClick(Player player, InventoryClickEvent event) { - if (cancelClicks) { - event.setCancelled(true); - event.setResult(Event.Result.DENY); - } - - ItemStack current = event.getCurrentItem(); - if (current == null) return; - - String id = current.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); - if (id == null) return; - - Button button = buttonById.get(UUID.fromString(id)); - if (button == null) return; - - button.processClickAction(player, event); + MenuInteractionUtil.processClick(cancelClicks, buttonById, player, event); } /** @@ -115,13 +100,7 @@ public void onClick(Player player, InventoryClickEvent event) { */ @Override public void open() { - refresh(); - - UserInterface.invalidateFromCache(player.getUniqueId()); - - CACHE.put(player.getUniqueId(), this); - - Scheduler.entity(player).execute(() -> inventory.open(), 1); + MenuInteractionUtil.openMenu(player, this, this::refresh, () -> inventory); } /** @@ -173,18 +152,7 @@ public Collection> getRefreshingButtons() { * @param button The button to refresh */ public void refreshButton(int slot, Button button) { - if (inventory == null) return; - - Player player = (Player) inventory.getPlayer(); - - ItemStack stack = button.item(player); - if (stack == null || stack.getType().isAir()) return; - - stack.editPersistentDataContainer( - data -> data.set(Button.KEY, PersistentDataType.STRING, button.uuid().toString()) - ); - - SafeUtil.setInventoryItem(inventory, slot, stack); + MenuInteractionUtil.refreshButton(inventory, slot, button); } /** @@ -193,12 +161,7 @@ public void refreshButton(int slot, Button button) { * @param button The button to add */ public void addButton(int slot, Button button) { - buttons.put(slot, button); - buttonById.put(button.uuid(), button); - - if (button.refreshIntervalTicks() <= 0L) return; - - refreshingButtons.put(slot, button); + MenuInteractionUtil.addButton(slot, button, buttons, buttonById, refreshingButtons); } /** @@ -299,16 +262,7 @@ public boolean cancelClicks() { * @return true if the click should be cancelled */ public boolean checkCancelClick(int slot) { - ItemStack item = inventory.getItem(slot); - if (item == null || item.getType().isAir()) return false; - - String id = item.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); - if (id == null) return false; - - Button button = buttonById.get(UUID.fromString(id)); - if (button == null) return false; - - return button.cancelClick(); + return MenuInteractionUtil.checkCancelClick(inventory, buttonById, slot); } } diff --git a/paper/src/main/java/games/negative/engine/paper/gui/PaginatedMenu.java b/paper/src/main/java/games/negative/engine/paper/gui/PaginatedMenu.java index 494c5f5..a506fb0 100644 --- a/paper/src/main/java/games/negative/engine/paper/gui/PaginatedMenu.java +++ b/paper/src/main/java/games/negative/engine/paper/gui/PaginatedMenu.java @@ -3,8 +3,8 @@ import com.google.common.base.Preconditions; import games.negative.engine.message.util.MiniMessageUtil; import games.negative.engine.paper.gui.button.Button; +import games.negative.engine.paper.gui.util.MenuInteractionUtil; import games.negative.engine.paper.gui.util.SafeUtil; -import games.negative.engine.paper.scheduler.Scheduler; import lombok.extern.slf4j.Slf4j; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -200,16 +200,7 @@ public void onClose(Player player, InventoryCloseEvent event) { */ @Override public void onClick(Player player, InventoryClickEvent event) { - ItemStack current = event.getCurrentItem(); - if (current == null) return; - - String id = current.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); - if (id == null) return; - - Button button = buttonById.get(UUID.fromString(id)); - if (button == null) return; - - button.processClickAction(player, event); + MenuInteractionUtil.processClick(false, buttonById, player, event); } /** @@ -219,13 +210,7 @@ public void onClick(Player player, InventoryClickEvent event) { */ @Override public void open() { - refresh(); - - UserInterface.invalidateFromCache(player.getUniqueId()); - - CACHE.put(player.getUniqueId(), this); - - Scheduler.entity(player).execute(() -> inventory.open(), 1); + MenuInteractionUtil.openMenu(player, this, this::refresh, () -> inventory); } /** @@ -339,12 +324,7 @@ public void addButton(int slot, Button button) { "Slot must be between 0 and " + (rows * 9 - 1) ); - buttons.put(slot, button); - buttonById.put(button.uuid(), button); - - if (button.refreshIntervalTicks() <= 0L) return; - - refreshingButtons.put(slot, button); + MenuInteractionUtil.addButton(slot, button, buttons, buttonById, refreshingButtons); } /** @@ -502,16 +482,7 @@ public boolean cancelClicks() { * @return true if the click should be cancelled */ public boolean checkCancelClick(int slot) { - ItemStack item = inventory.getItem(slot); - if (item == null || item.getType().isAir()) return false; - - String id = item.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); - if (id == null) return false; - - Button button = buttonById.get(UUID.fromString(id)); - if (button == null) return false; - - return button.cancelClick(); + return MenuInteractionUtil.checkCancelClick(inventory, buttonById, slot); } /** diff --git a/paper/src/main/java/games/negative/engine/paper/gui/util/MenuInteractionUtil.java b/paper/src/main/java/games/negative/engine/paper/gui/util/MenuInteractionUtil.java new file mode 100644 index 0000000..f0a50d7 --- /dev/null +++ b/paper/src/main/java/games/negative/engine/paper/gui/util/MenuInteractionUtil.java @@ -0,0 +1,105 @@ +package games.negative.engine.paper.gui.util; + +import games.negative.engine.paper.gui.UserInterface; +import games.negative.engine.paper.gui.button.Button; +import games.negative.engine.paper.scheduler.Scheduler; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; + +public final class MenuInteractionUtil { + + private MenuInteractionUtil() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static void processClick( + boolean cancelClicks, + Map buttonById, + Player player, + InventoryClickEvent event + ) { + if (cancelClicks) { + event.setCancelled(true); + event.setResult(Event.Result.DENY); + } + + ItemStack current = event.getCurrentItem(); + if (current == null) return; + + String id = current.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); + if (id == null) return; + + Button button = buttonById.get(UUID.fromString(id)); + if (button == null) return; + + button.processClickAction(player, event); + } + + public static void openMenu( + Player player, + UserInterface ui, + Runnable refreshAction, + Supplier inventorySupplier + ) { + refreshAction.run(); + + UserInterface.invalidateFromCache(player.getUniqueId()); + UserInterface.CACHE.put(player.getUniqueId(), ui); + + Scheduler.entity(player).execute(() -> { + InventoryView view = inventorySupplier.get(); + if (view != null) view.open(); + }, 1); + } + + public static void refreshButton(InventoryView inventory, int slot, Button button) { + if (inventory == null) return; + + Player player = (Player) inventory.getPlayer(); + ItemStack stack = button.item(player); + if (stack == null || stack.getType().isAir()) return; + + stack.editPersistentDataContainer( + data -> data.set(Button.KEY, PersistentDataType.STRING, button.uuid().toString()) + ); + + SafeUtil.setInventoryItem(inventory, slot, stack); + } + + public static void addButton( + int slot, + Button button, + Map buttons, + Map buttonById, + Map refreshingButtons + ) { + buttons.put(slot, button); + buttonById.put(button.uuid(), button); + + if (button.refreshIntervalTicks() <= 0L) return; + refreshingButtons.put(slot, button); + } + + public static boolean checkCancelClick(InventoryView inventory, Map buttonById, int slot) { + if (inventory == null) return false; + + ItemStack item = inventory.getItem(slot); + if (item == null || item.getType().isAir()) return false; + + String id = item.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING); + if (id == null) return false; + + Button button = buttonById.get(UUID.fromString(id)); + if (button == null) return false; + + return button.cancelClick(); + } +} diff --git a/paper/src/main/java/games/negative/engine/paper/platform/PaperLocalizationPlatform.java b/paper/src/main/java/games/negative/engine/paper/platform/PaperLocalizationPlatform.java index 331081b..7112e67 100644 --- a/paper/src/main/java/games/negative/engine/paper/platform/PaperLocalizationPlatform.java +++ b/paper/src/main/java/games/negative/engine/paper/platform/PaperLocalizationPlatform.java @@ -1,7 +1,6 @@ package games.negative.engine.paper.platform; import games.negative.engine.message.LocalizationPlatform; -import games.negative.engine.message.util.PlaceholderAPIUtil; import games.negative.moss.spring.SpringComponent; import me.clip.placeholderapi.PlaceholderAPI; import net.kyori.adventure.audience.Audience; diff --git a/paper/src/main/java/games/negative/engine/paper/util/JsonUtil.java b/paper/src/main/java/games/negative/engine/paper/util/JsonUtil.java index 01628c3..924714a 100644 --- a/paper/src/main/java/games/negative/engine/paper/util/JsonUtil.java +++ b/paper/src/main/java/games/negative/engine/paper/util/JsonUtil.java @@ -4,10 +4,19 @@ import com.google.gson.Gson; import lombok.extern.slf4j.Slf4j; -import java.io.*; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; /** * Utility class for JSON operations using Gson.