Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cfee3ce39 | ||
|
|
f69378fb2b | ||
|
|
695c78ea16 |
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
cache: gradle
|
||||
|
||||
- name: Validate conventional commits
|
||||
@@ -35,6 +35,6 @@ jobs:
|
||||
- name: Upload development artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: spigot-tyrant-${{ github.sha }}
|
||||
name: purpur-tyrant-${{ github.sha }}
|
||||
path: build/libs/*.jar
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
cache: gradle
|
||||
|
||||
- name: Set up Node.js
|
||||
@@ -86,8 +86,8 @@ jobs:
|
||||
if: steps.release.outputs.created == 'true'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: spigot-tyrant-${{ steps.release.outputs.version }}
|
||||
path: build/libs/spigot-tyrant-${{ steps.release.outputs.version }}.jar
|
||||
name: purpur-tyrant-${{ steps.release.outputs.version }}
|
||||
path: build/libs/purpur-tyrant-${{ steps.release.outputs.version }}.jar
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create Gitea release and upload plugin
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
PREVIOUS_TAG: ${{ steps.previous_tag.outputs.value }}
|
||||
run: |
|
||||
api_url="${GITEA_SERVER_URL}/api/v1"
|
||||
jar="build/libs/spigot-tyrant-${VERSION}.jar"
|
||||
jar="build/libs/purpur-tyrant-${VERSION}.jar"
|
||||
export RELEASE_BODY
|
||||
if [ -n "$PREVIOUS_TAG" ]; then
|
||||
RELEASE_BODY=$(git log --pretty='format:- %s (%h)' "${PREVIOUS_TAG}..HEAD")
|
||||
@@ -138,4 +138,4 @@ jobs:
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${jar}" \
|
||||
"${api_url}/repos/${REPOSITORY}/releases/${release_id}/assets?name=spigot-tyrant-${VERSION}.jar"
|
||||
"${api_url}/repos/${REPOSITORY}/releases/${release_id}/assets?name=purpur-tyrant-${VERSION}.jar"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# spigot-tyrant agent entrypoint
|
||||
# purpur-tyrant agent entrypoint
|
||||
|
||||
The canonical stories, engineering guidance, and **all process documents** are in the private [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md).
|
||||
|
||||
Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/spigot-tyrant/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/spigot-tyrant/` stories. Also follow the parent workspace `AGENTS.md` when present.
|
||||
Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/purpur-tyrant/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/purpur-tyrant/` stories. Also follow the parent workspace `AGENTS.md` when present.
|
||||
|
||||
For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/spigot-tyrant/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access.
|
||||
For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-tyrant/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access.
|
||||
|
||||
Development follows [Development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md): approved stories, failing tests, passing implementation, verification, then source/wiki commit and push. GitOps updates are committed locally **without pushing**; only [Do release](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/do-release.md) authorizes a reviewed GitOps push.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Purpur Tyrant
|
||||
|
||||
The asymmetric Tyrant/Vigilante game plugin, targeting Java 25 and Purpur 26.2 build 2618.
|
||||
|
||||
## Build and releases
|
||||
|
||||
```sh
|
||||
./gradlew clean check jar
|
||||
```
|
||||
|
||||
New artifacts use `build/libs/purpur-tyrant-<version>.jar`. Gitea CI checks pushes/pull requests; approved conventional commits on main drive versioned releases. Distribution tests inspect the actual JAR, version, Java 25 bytecode, retained runtime identity and workflow names.
|
||||
|
||||
## Migration compatibility
|
||||
|
||||
The repository and checkout are now `purpur-tyrant`, previously `spigot-tyrant`. Runtime identity remains **SpigotTyrant**, including `plugins/SpigotTyrant/`, Java entrypoints/packages, command/permission names and persistent keys for roles, captured mobs and legacy items. The migration changes the build/distribution target, not gameplay or persisted schemas. New releases no longer support Java 17.
|
||||
|
||||
Replace the old plugin JAR rather than installing both distributions. Old tags/assets remain available; both the origin GitOps v1.24.0 and separately prepared local v1.25.0 URLs were verified to redirect and download identical bytes after the repository rename. No deployment or GitOps push is implied by publication.
|
||||
|
||||
Unrelated local testing files are preserved; their commands may still reference the historical artifact names.
|
||||
|
||||
See the [canonical project](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-tyrant/index.md), [stories](https://git.garvis.dev/dmg/somc-okf/src/branch/main/user-stories/purpur-tyrant/index.md), and [development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md).
|
||||
+9
-5
@@ -9,23 +9,24 @@ version = providers.gradleProperty("releaseVersion")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
|
||||
maven("https://repo.purpurmc.org/snapshots/")
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror"))
|
||||
// Retain supported legacy Bukkit APIs while preserving runtime compatibility.
|
||||
options.compilerArgs.addAll(listOf("-Xlint:all,-deprecation,-removal", "-Werror"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT")
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
|
||||
testImplementation("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT")
|
||||
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testImplementation("org.mockito:mockito-core:5.18.0")
|
||||
@@ -35,6 +36,9 @@ dependencies {
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
dependsOn(tasks.jar)
|
||||
systemProperty("distribution.jar", tasks.jar.get().archiveFile.get().asFile.absolutePath)
|
||||
systemProperty("distribution.version", project.version.toString())
|
||||
}
|
||||
|
||||
val pluginVersion = version
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
rootProject.name = "spigot-tyrant"
|
||||
rootProject.name = "purpur-tyrant"
|
||||
|
||||
@@ -79,6 +79,18 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
|
||||
game, player.getUniqueId()
|
||||
);
|
||||
boolean retained = false;
|
||||
ItemStack cursor = player.getItemOnCursor();
|
||||
if (isBoundRoleControlItem(cursor)) {
|
||||
if (owner(cursor).filter(player.getUniqueId()::equals).isPresent() && role(cursor).equals(required)) {
|
||||
retained = true; // Preserve the copy the owner is actively moving.
|
||||
if (cursor.getAmount() > 1) {
|
||||
cursor.setAmount(1);
|
||||
player.setItemOnCursor(cursor);
|
||||
}
|
||||
} else {
|
||||
player.setItemOnCursor(null);
|
||||
}
|
||||
}
|
||||
ItemStack[] contents = player.getInventory().getContents();
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
ItemStack item = contents[index];
|
||||
@@ -90,6 +102,10 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
|
||||
&& role(item).equals(required);
|
||||
if (valid) {
|
||||
retained = true;
|
||||
if (item.getAmount() > 1) {
|
||||
item.setAmount(1);
|
||||
player.getInventory().setItem(index, item);
|
||||
}
|
||||
} else {
|
||||
player.getInventory().setItem(index, null);
|
||||
}
|
||||
@@ -98,6 +114,9 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
|
||||
|
||||
@Override
|
||||
public void removeAll(Player player) {
|
||||
if (isBoundRoleControlItem(player.getItemOnCursor())) {
|
||||
player.setItemOnCursor(null);
|
||||
}
|
||||
ItemStack[] contents = player.getInventory().getContents();
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
if (isBoundRoleControlItem(contents[index])) {
|
||||
@@ -127,6 +146,11 @@ public final class BukkitRoleControlItemService implements RoleControlItemServic
|
||||
}
|
||||
|
||||
private boolean contains(Player player, RoleControl role) {
|
||||
ItemStack cursor = player.getItemOnCursor();
|
||||
if (owner(cursor).filter(player.getUniqueId()::equals).isPresent()
|
||||
&& role(cursor).filter(role::equals).isPresent()) {
|
||||
return true;
|
||||
}
|
||||
for (ItemStack item : player.getInventory().getContents()) {
|
||||
if (owner(item).filter(player.getUniqueId()::equals).isPresent()
|
||||
&& role(item).filter(role::equals).isPresent()) {
|
||||
|
||||
@@ -31,6 +31,19 @@ public final class CapturedMobInventoryService {
|
||||
}
|
||||
}
|
||||
Set<UUID> retained = new HashSet<>();
|
||||
ItemStack cursor = player.getItemOnCursor();
|
||||
if (items.isCapturedMob(cursor)) {
|
||||
UUID captureId = items.captureId(cursor).orElseThrow();
|
||||
if (items.owner(cursor).filter(state.playerId()::equals).isPresent()
|
||||
&& stored.containsKey(captureId) && retained.add(captureId)) {
|
||||
if (cursor.getAmount() > 1) {
|
||||
cursor.setAmount(1);
|
||||
player.setItemOnCursor(cursor);
|
||||
}
|
||||
} else {
|
||||
player.setItemOnCursor(null);
|
||||
}
|
||||
}
|
||||
ItemStack[] contents = player.getInventory().getContents();
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
ItemStack item = contents[index];
|
||||
@@ -42,6 +55,9 @@ public final class CapturedMobInventoryService {
|
||||
&& stored.containsKey(captureId) && retained.add(captureId);
|
||||
if (!valid) {
|
||||
player.getInventory().setItem(index, null);
|
||||
} else if (item.getAmount() > 1) {
|
||||
item.setAmount(1);
|
||||
player.getInventory().setItem(index, item);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<UUID, CapturedMob> entry : stored.entrySet()) {
|
||||
|
||||
@@ -89,12 +89,28 @@ public final class RoleControlItemListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
Player player = event.getWhoClicked() instanceof Player clicked ? clicked : null;
|
||||
ItemStack swapSource = null;
|
||||
if (player != null) {
|
||||
if (event.getHotbarButton() >= 0 && event.getHotbarButton() < 9) {
|
||||
swapSource = player.getInventory().getItem(event.getHotbarButton());
|
||||
} else if (event.getClick() == org.bukkit.event.inventory.ClickType.SWAP_OFFHAND) {
|
||||
swapSource = player.getInventory().getItemInOffHand();
|
||||
}
|
||||
}
|
||||
if (items.isBoundRoleControlItem(event.getCurrentItem())
|
||||
|| items.isBoundRoleControlItem(event.getCursor())) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)
|
||||
|| event.isShiftClick()
|
||||
|| event.getClickedInventory() == null
|
||||
|| !event.getClickedInventory().equals(player.getInventory())) {
|
||||
|| items.isBoundRoleControlItem(event.getCursor()) || items.isBoundRoleControlItem(swapSource)) {
|
||||
var action = event.getAction();
|
||||
boolean scansOtherSlots = event.isShiftClick()
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.MOVE_TO_OTHER_INVENTORY
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.COLLECT_TO_CURSOR;
|
||||
if (player == null || event.getClickedInventory() == null
|
||||
|| !event.getClickedInventory().equals(player.getInventory())
|
||||
|| !validFor(player, event.getCurrentItem()) || !validFor(player, event.getCursor())
|
||||
|| !validFor(player, swapSource)
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.CLONE_STACK
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.UNKNOWN
|
||||
|| scansOtherSlots && !personalCraftingView(event.getView())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
@@ -105,10 +121,28 @@ public final class RoleControlItemListener implements Listener {
|
||||
if (items.isBoundRoleControlItem(event.getOldCursor())
|
||||
|| event.getNewItems().values().stream()
|
||||
.anyMatch(items::isBoundRoleControlItem)) {
|
||||
event.setCancelled(true);
|
||||
if (!(event.getWhoClicked() instanceof Player player)
|
||||
|| !validFor(player, event.getOldCursor())
|
||||
|| event.getNewItems().values().stream().anyMatch(item -> !validFor(player, item))
|
||||
|| event.getRawSlots().stream().anyMatch(raw ->
|
||||
!player.getInventory().equals(event.getView().getInventory(raw)))) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean validFor(Player player, ItemStack item) {
|
||||
return !items.isBoundRoleControlItem(item)
|
||||
|| items.owner(item).filter(player.getUniqueId()::equals).isPresent()
|
||||
&& items.role(item).equals(RoleControlItemPolicy.requiredRole(stateManager.game(), player.getUniqueId()));
|
||||
}
|
||||
|
||||
private static boolean personalCraftingView(org.bukkit.inventory.InventoryView view) {
|
||||
// The native player view has a 2x2 matrix; a workbench has 3x3 and is an external destination.
|
||||
return view.getTopInventory() instanceof org.bukkit.inventory.CraftingInventory crafting
|
||||
&& crafting.getMatrix().length == 4;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryMove(InventoryMoveItemEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getItem())) {
|
||||
|
||||
@@ -205,7 +205,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new TamerListener(
|
||||
stateManager, abilityItems, capturedMobItems, capturedMobs,
|
||||
settings, getServer()
|
||||
settings, getServer(), task -> getServer().getScheduler().runTask(this, task)
|
||||
),
|
||||
this
|
||||
);
|
||||
|
||||
@@ -33,6 +33,8 @@ public final class TamerListener implements Listener {
|
||||
private final CapturedMobService capturedMobs;
|
||||
private final PluginSettings settings;
|
||||
private final Server server;
|
||||
private final java.util.function.Consumer<Runnable> nextTick;
|
||||
private final java.util.Set<UUID> pendingInventoryRefresh = new java.util.HashSet<>();
|
||||
|
||||
public TamerListener(
|
||||
TyrantStateManager stateManager,
|
||||
@@ -40,7 +42,8 @@ public final class TamerListener implements Listener {
|
||||
CapturedMobItemService capturedItems,
|
||||
CapturedMobService capturedMobs,
|
||||
PluginSettings settings,
|
||||
Server server
|
||||
Server server,
|
||||
java.util.function.Consumer<Runnable> nextTick
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.abilityItems = abilityItems;
|
||||
@@ -48,6 +51,7 @@ public final class TamerListener implements Listener {
|
||||
this.capturedMobs = capturedMobs;
|
||||
this.settings = settings;
|
||||
this.server = server;
|
||||
this.nextTick = nextTick;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
@@ -162,15 +166,82 @@ public final class TamerListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (capturedItems.isCapturedMob(event.getCurrentItem())
|
||||
|| capturedItems.isCapturedMob(event.getCursor())) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)
|
||||
|| event.isShiftClick()
|
||||
|| event.getClickedInventory() == null
|
||||
|| !event.getClickedInventory().equals(player.getInventory())) {
|
||||
event.setCancelled(true);
|
||||
Player player = event.getWhoClicked() instanceof Player clicked ? clicked : null;
|
||||
ItemStack swapSource = null;
|
||||
if (player != null) {
|
||||
if (event.getHotbarButton() >= 0 && event.getHotbarButton() < 9) {
|
||||
swapSource = player.getInventory().getItem(event.getHotbarButton());
|
||||
} else if (event.getClick() == org.bukkit.event.inventory.ClickType.SWAP_OFFHAND) {
|
||||
swapSource = player.getInventory().getItemInOffHand();
|
||||
}
|
||||
}
|
||||
if (capturedItems.isCapturedMob(event.getCurrentItem())
|
||||
|| capturedItems.isCapturedMob(event.getCursor()) || capturedItems.isCapturedMob(swapSource)) {
|
||||
var action = event.getAction();
|
||||
if (player == null || event.isCancelled() || event.isShiftClick()
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.MOVE_TO_OTHER_INVENTORY
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.CLONE_STACK
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.COLLECT_TO_CURSOR
|
||||
|| action == org.bukkit.event.inventory.InventoryAction.UNKNOWN
|
||||
|| event.getClickedInventory() == null
|
||||
|| !event.getClickedInventory().equals(player.getInventory())
|
||||
|| !ownedBy(player, event.getCurrentItem()) || !ownedBy(player, event.getCursor())
|
||||
|| !ownedBy(player, swapSource)) {
|
||||
event.setCancelled(true);
|
||||
if (player != null) {
|
||||
refreshInventory(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryDrag(org.bukkit.event.inventory.InventoryDragEvent event) {
|
||||
if (!capturedItems.isCapturedMob(event.getOldCursor())
|
||||
&& event.getNewItems().values().stream().noneMatch(capturedItems::isCapturedMob)) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getWhoClicked() instanceof Player dragged ? dragged : null;
|
||||
if (player == null || event.isCancelled() || !ownedBy(player, event.getOldCursor())
|
||||
|| event.getNewItems().values().stream().anyMatch(item -> !ownedBy(player, item))
|
||||
|| event.getRawSlots().stream().anyMatch(raw ->
|
||||
!player.getInventory().equals(event.getView().getInventory(raw)))) {
|
||||
event.setCancelled(true);
|
||||
if (player != null) {
|
||||
refreshInventory(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryMove(org.bukkit.event.inventory.InventoryMoveItemEvent event) {
|
||||
if (capturedItems.isCapturedMob(event.getItem())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ownedBy(Player player, ItemStack item) {
|
||||
return !capturedItems.isCapturedMob(item)
|
||||
|| capturedItems.owner(item).filter(player.getUniqueId()::equals).isPresent();
|
||||
}
|
||||
|
||||
private void refreshInventory(Player player) {
|
||||
UUID id = player.getUniqueId();
|
||||
if (!pendingInventoryRefresh.add(id)) {
|
||||
return;
|
||||
}
|
||||
// The native click transaction must finish before forcing its authoritative contents to the client.
|
||||
try {
|
||||
nextTick.accept(() -> {
|
||||
pendingInventoryRefresh.remove(id);
|
||||
if (player.isOnline()) {
|
||||
player.updateInventory();
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException exception) {
|
||||
pendingInventoryRefresh.remove(id);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
class BukkitRoleControlItemServiceTest {
|
||||
private final UUID owner = UUID.randomUUID();
|
||||
private final Player player = mock(Player.class);
|
||||
private final PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
private final ItemStack[] contents = new ItemStack[41];
|
||||
private final AtomicReference<ItemStack> cursor = new AtomicReference<>();
|
||||
private BukkitRoleControlItemService service;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
Plugin plugin = mock(Plugin.class);
|
||||
when(plugin.getName()).thenReturn("SpigotTyrant");
|
||||
when(plugin.namespace()).thenReturn("spigottyrant");
|
||||
service = new BukkitRoleControlItemService(plugin, PluginSettings.from(Map.of()));
|
||||
when(player.getUniqueId()).thenReturn(owner);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(inventory.getContents()).thenAnswer(call -> contents.clone());
|
||||
doAnswer(call -> { contents[call.getArgument(0, Integer.class)] = call.getArgument(1); return null; })
|
||||
.when(inventory).setItem(anyInt(), nullable(ItemStack.class));
|
||||
when(player.getItemOnCursor()).thenAnswer(call -> cursor.get());
|
||||
doAnswer(call -> { cursor.set(call.getArgument(0)); return null; })
|
||||
.when(player).setItemOnCursor(nullable(ItemStack.class));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void recoveryDoesNotDuplicateOrRepositionCursorHeldControlItem(RoleControl role) {
|
||||
ItemStack held = item(owner, role);
|
||||
cursor.set(held);
|
||||
try (var created = mockConstruction(ItemStack.class, (item, context) -> {
|
||||
ItemMeta meta = mock(ItemMeta.class);
|
||||
when(item.getItemMeta()).thenReturn(meta);
|
||||
when(meta.getPersistentDataContainer()).thenReturn(mock(PersistentDataContainer.class));
|
||||
})) {
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
service.recover(player, game(role));
|
||||
}
|
||||
assertEquals(0, created.constructed().size(), "Cursor-held item already satisfies recovery");
|
||||
assertSame(held, cursor.get());
|
||||
verify(inventory, never()).addItem(any(ItemStack[].class));
|
||||
verify(inventory, never()).setItem(anyInt(), nullable(ItemStack.class));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void refreshPreservesCursorChoiceAndRemovesExtraInventoryCopies(RoleControl role) {
|
||||
ItemStack held = item(owner, role);
|
||||
cursor.set(held);
|
||||
contents[5] = item(owner, role);
|
||||
ItemStack ordinary = mock(ItemStack.class);
|
||||
contents[8] = ordinary;
|
||||
var manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game(role));
|
||||
var server = mock(org.bukkit.Server.class);
|
||||
doReturn(java.util.List.of(player)).when(server).getOnlinePlayers();
|
||||
var refresh = new RoleControlItemRefreshTask(manager, service, server);
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
refresh.run();
|
||||
}
|
||||
assertSame(held, cursor.get());
|
||||
assertNull(contents[5], "Keep the actively moved cursor copy, not an old recovery duplicate");
|
||||
assertSame(ordinary, contents[8]);
|
||||
verify(inventory, never()).addItem(any(ItemStack[].class));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void roleLossRemovesCursorAndInventoryCopies(RoleControl role) {
|
||||
cursor.set(item(owner, role));
|
||||
contents[5] = item(owner, role);
|
||||
service.removeInvalid(player, game(null));
|
||||
assertNull(cursor.get());
|
||||
assertNull(contents[5]);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void explicitRemovalIncludesCursorWithoutTouchingOrdinaryItems(RoleControl role) {
|
||||
cursor.set(item(owner, role));
|
||||
contents[5] = item(owner, role);
|
||||
ItemStack ordinary = mock(ItemStack.class);
|
||||
contents[8] = ordinary;
|
||||
service.removeAll(player);
|
||||
assertNull(cursor.get());
|
||||
assertNull(contents[5]);
|
||||
assertSame(ordinary, contents[8]);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void mergedDuplicateStacksAreReducedWithoutMovingTheCanonicalItem(RoleControl role) {
|
||||
for (boolean heldOnCursor : new boolean[] {false, true}) {
|
||||
java.util.Arrays.fill(contents, null);
|
||||
cursor.set(null);
|
||||
ItemStack held = item(owner, role);
|
||||
held.setAmount(2);
|
||||
if (heldOnCursor) { cursor.set(held); } else { contents[5] = held; }
|
||||
service.removeInvalid(player, game(role));
|
||||
assertEquals(1, held.getAmount(), "Recovery duplicates may have merged into one stack");
|
||||
assertSame(held, heldOnCursor ? cursor.get() : contents[5]);
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack item(UUID playerId, RoleControl role) {
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
ItemMeta meta = mock(ItemMeta.class);
|
||||
PersistentDataContainer data = mock(PersistentDataContainer.class);
|
||||
when(item.hasItemMeta()).thenReturn(true);
|
||||
when(item.getItemMeta()).thenReturn(meta);
|
||||
var amount = new java.util.concurrent.atomic.AtomicInteger(1);
|
||||
when(item.getAmount()).thenAnswer(call -> amount.get());
|
||||
doAnswer(call -> { amount.set(call.getArgument(0)); return null; }).when(item).setAmount(anyInt());
|
||||
when(meta.getPersistentDataContainer()).thenReturn(data);
|
||||
when(data.get(new NamespacedKey("spigottyrant", "role-control"), PersistentDataType.STRING))
|
||||
.thenReturn(role.name());
|
||||
when(data.get(new NamespacedKey("spigottyrant", "role-control-owner"), PersistentDataType.STRING))
|
||||
.thenReturn(playerId.toString());
|
||||
return item;
|
||||
}
|
||||
|
||||
private GameState game(RoleControl role) {
|
||||
return new GameState(GameLifecycle.RUNNING,
|
||||
role == RoleControl.TYRANT ? Optional.of(owner) : Optional.empty(),
|
||||
role == RoleControl.VIGILANTE ? Optional.of(owner) : Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryAction;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryView;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class CapturedMobInventorySynchronizationTest {
|
||||
@TempDir Path directory;
|
||||
private final UUID owner = UUID.randomUUID(), captureId = UUID.randomUUID();
|
||||
private final Player player = mock(Player.class);
|
||||
private final PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
private final Inventory top = mock(Inventory.class);
|
||||
private final InventoryView view = mock(InventoryView.class);
|
||||
private final Server server = mock(Server.class);
|
||||
private final ItemStack[] contents = new ItemStack[41];
|
||||
private final AtomicReference<ItemStack> cursor = new AtomicReference<>();
|
||||
private final List<Runnable> scheduled = new ArrayList<>();
|
||||
private TyrantStateManager manager;
|
||||
private CapturedMobItemService items;
|
||||
private TamerListener listener;
|
||||
private ItemStack egg;
|
||||
private Path stateFile;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
when(player.getUniqueId()).thenReturn(owner);
|
||||
when(player.getName()).thenReturn("Tamer");
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(player.getItemOnCursor()).thenAnswer(call -> cursor.get());
|
||||
doAnswer(call -> { cursor.set(call.getArgument(0)); return null; })
|
||||
.when(player).setItemOnCursor(nullable(ItemStack.class));
|
||||
when(inventory.getContents()).thenAnswer(call -> contents.clone());
|
||||
when(inventory.getItem(anyInt())).thenAnswer(call -> contents[call.getArgument(0, Integer.class)]);
|
||||
doAnswer(call -> { contents[call.getArgument(0, Integer.class)] = call.getArgument(1); return null; })
|
||||
.when(inventory).setItem(anyInt(), nullable(ItemStack.class));
|
||||
when(view.getPlayer()).thenReturn(player);
|
||||
when(view.getBottomInventory()).thenReturn(inventory);
|
||||
when(view.getTopInventory()).thenReturn(top);
|
||||
when(top.getSize()).thenReturn(27);
|
||||
when(top.getContents()).thenReturn(new ItemStack[27]);
|
||||
when(view.getInventory(anyInt())).thenAnswer(call -> {
|
||||
int raw = call.getArgument(0);
|
||||
return raw < 0 || raw >= 68 ? null : raw < 27 ? top : inventory;
|
||||
});
|
||||
when(view.convertSlot(anyInt())).thenAnswer(call -> call.getArgument(0, Integer.class) - 27);
|
||||
when(view.getItem(anyInt())).thenAnswer(call -> {
|
||||
int raw = call.getArgument(0);
|
||||
return raw >= 27 && raw < 68 ? contents[raw - 27] : null;
|
||||
});
|
||||
when(view.getCursor()).thenAnswer(call -> cursor.get());
|
||||
Plugin plugin = mock(Plugin.class);
|
||||
when(plugin.getName()).thenReturn("SpigotTyrant");
|
||||
when(plugin.namespace()).thenReturn("spigottyrant");
|
||||
items = new BukkitCapturedMobItemService(plugin, PluginSettings.from(Map.of()));
|
||||
egg = egg(owner, captureId);
|
||||
contents[3] = egg;
|
||||
stateFile = directory.resolve("state.yml");
|
||||
manager = new TyrantStateManager(new YamlTyrantStateRepository(stateFile), Logger.getAnonymousLogger());
|
||||
var mob = new CapturedMob("COW", Map.of("capture-id", captureId.toString(), "snapshot", "stored-cow"));
|
||||
var state = new PlayerState(owner, "Tamer", Optional.empty(), Optional.empty(), TyrantClass.TAMER,
|
||||
Optional.empty(), Map.of(), Set.of(), List.of(mob));
|
||||
manager.updatePlayer(owner, "Tamer", ignored -> state);
|
||||
assertTrue(manager.saveIfDirty());
|
||||
listener = new TamerListener(manager, mock(AbilityItemService.class), items,
|
||||
new CapturedMobService(), PluginSettings.from(Map.of()), server, scheduled::add);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectedShiftKeepsAuthoritativeEggAndCustodyAndResendsAfterTransaction() throws Exception {
|
||||
var before = manager.snapshot();
|
||||
byte[] bytes = Files.readAllBytes(stateFile);
|
||||
var event = click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled());
|
||||
assertSame(egg, contents[3]);
|
||||
assertTrue(java.util.Arrays.stream(top.getContents()).allMatch(java.util.Objects::isNull));
|
||||
verify(player, never()).updateInventory();
|
||||
assertEquals(1, scheduled.size(), "A rejected transfer needs a post-transaction full inventory refresh");
|
||||
scheduled.remove(0).run();
|
||||
verify(player).updateInventory();
|
||||
assertEquals(before, manager.snapshot());
|
||||
assertArrayEquals(bytes, Files.readAllBytes(stateFile));
|
||||
assertEquals(1, new YamlTyrantStateRepository(stateFile).load().players().get(owner).capturedMobs().size());
|
||||
verify(server, never()).getEntityFactory();
|
||||
verify(inventory, never()).setItem(anyInt(), nullable(ItemStack.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedRejectedTransfersCoalesceRefreshWithoutChangingEggOrCustody() throws Exception {
|
||||
var before = manager.snapshot();
|
||||
byte[] bytes = Files.readAllBytes(stateFile);
|
||||
for (int tick = 0; tick < 3; tick++) {
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
var event = click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled());
|
||||
assertSame(egg, contents[3]);
|
||||
}
|
||||
assertEquals(1, scheduled.size(), "Repeated rejects in one tick need one authoritative refresh");
|
||||
scheduled.remove(0).run();
|
||||
}
|
||||
verify(player, times(3)).updateInventory();
|
||||
assertEquals(before, manager.snapshot());
|
||||
assertArrayEquals(bytes, Files.readAllBytes(stateFile));
|
||||
assertEquals(1, egg.getAmount());
|
||||
assertTrue(java.util.Arrays.stream(top.getContents()).allMatch(java.util.Objects::isNull));
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutBeforeRefreshDoesNotMutateOrReissueAnything() {
|
||||
var before = manager.snapshot();
|
||||
listener.onInventoryClick(click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1));
|
||||
when(player.isOnline()).thenReturn(false);
|
||||
scheduled.remove(0).run();
|
||||
verify(player, never()).updateInventory();
|
||||
assertSame(egg, contents[3]);
|
||||
assertEquals(before, manager.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectedSchedulingDoesNotPermanentlySuppressLaterRefresh() {
|
||||
int[] attempts = {0};
|
||||
listener = new TamerListener(manager, mock(AbilityItemService.class), items,
|
||||
new CapturedMobService(), PluginSettings.from(Map.of()), server, task -> {
|
||||
if (attempts[0]++ == 0) { throw new IllegalStateException("scheduler unavailable"); }
|
||||
scheduled.add(task);
|
||||
});
|
||||
var first = click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
assertThrows(IllegalStateException.class, () -> listener.onInventoryClick(first));
|
||||
assertTrue(first.isCancelled());
|
||||
listener.onInventoryClick(click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1));
|
||||
assertEquals(1, scheduled.size());
|
||||
scheduled.remove(0).run();
|
||||
verify(player).updateInventory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void recoveryDoesNotDuplicateAnEggHeldOnTheCursor() {
|
||||
contents[3] = null;
|
||||
cursor.set(egg);
|
||||
when(inventory.addItem(any(ItemStack[].class))).thenAnswer(call -> {
|
||||
ItemStack[] added = (ItemStack[]) call.getRawArguments()[0];
|
||||
for (ItemStack item : added) {
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
if (contents[index] == null) { contents[index] = item; break; }
|
||||
}
|
||||
}
|
||||
return new java.util.HashMap<Integer, ItemStack>();
|
||||
});
|
||||
var before = manager.snapshot();
|
||||
var recovery = new CapturedMobInventoryService(items, PluginSettings.from(Map.of()));
|
||||
try (var constructed = mockConstruction(ItemStack.class, (item, context) -> markEgg(item, owner, captureId))) {
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
recovery.reconcile(player, manager.player(owner, "Tamer"));
|
||||
}
|
||||
assertEquals(0, constructed.constructed().size(), "Cursor-held egg already represents the stored mob");
|
||||
assertTrue(java.util.Arrays.stream(contents).allMatch(java.util.Objects::isNull));
|
||||
assertSame(egg, cursor.get());
|
||||
assertEquals(before, manager.snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconciliationKeepsCursorCopyAndRemovesOnlyDuplicateEggs() {
|
||||
cursor.set(egg);
|
||||
egg.setAmount(2);
|
||||
contents[3] = egg(owner, captureId);
|
||||
ItemStack ordinary = mock(ItemStack.class);
|
||||
contents[4] = ordinary;
|
||||
var before = manager.snapshot();
|
||||
new CapturedMobInventoryService(items, PluginSettings.from(Map.of()))
|
||||
.reconcile(player, manager.player(owner, "Tamer"));
|
||||
assertSame(egg, cursor.get());
|
||||
assertEquals(1, egg.getAmount());
|
||||
assertNull(contents[3]);
|
||||
assertSame(ordinary, contents[4]);
|
||||
assertEquals(before, manager.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cursorEggIsRemovedOnlyWhenCustodyIsGoneOrOwnershipIsInvalid() {
|
||||
contents[3] = null;
|
||||
cursor.set(egg);
|
||||
var released = new CapturedMobService().release(manager.player(owner, "Tamer"), captureId);
|
||||
var recovery = new CapturedMobInventoryService(items, PluginSettings.from(Map.of()));
|
||||
recovery.reconcile(player, released);
|
||||
assertNull(cursor.get(), "A released mob must not leave a cursor token behind");
|
||||
contents[3] = egg;
|
||||
cursor.set(egg(UUID.randomUUID(), captureId));
|
||||
recovery.reconcile(player, manager.player(owner, "Tamer"));
|
||||
assertNull(cursor.get(), "Foreign items cannot be retained");
|
||||
assertSame(egg, contents[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hiddenHotbarTransferIsRejectedAndResynchronized() {
|
||||
contents[3] = null;
|
||||
contents[2] = egg;
|
||||
var event = click(0, ClickType.NUMBER_KEY, InventoryAction.HOTBAR_SWAP, 2);
|
||||
listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled());
|
||||
assertEquals(1, scheduled.size());
|
||||
scheduled.remove(0).run();
|
||||
verify(player).updateInventory();
|
||||
assertSame(egg, contents[2]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hiddenOffhandTransferIsRejectedAndResynchronized() {
|
||||
contents[3] = null;
|
||||
contents[40] = egg;
|
||||
when(inventory.getItemInOffHand()).thenReturn(egg);
|
||||
var event = click(0, ClickType.SWAP_OFFHAND, InventoryAction.HOTBAR_SWAP, -1);
|
||||
listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled());
|
||||
assertEquals(1, scheduled.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossInventoryDragIsRejectedAndResynchronized() {
|
||||
cursor.set(egg);
|
||||
contents[3] = null;
|
||||
var event = new org.bukkit.event.inventory.InventoryDragEvent(view, null, egg, false, Map.of(0, egg, 30, egg));
|
||||
listener.onInventoryDrag(event);
|
||||
assertTrue(event.isCancelled());
|
||||
assertEquals(1, scheduled.size());
|
||||
assertSame(egg, cursor.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void automatedTransferCannotMoveCapturedEggs() {
|
||||
var hopper = new org.bukkit.event.inventory.InventoryMoveItemEvent(top, egg, mock(Inventory.class), true);
|
||||
listener.onInventoryMove(hopper);
|
||||
assertTrue(hopper.isCancelled());
|
||||
assertTrue(scheduled.isEmpty(), "Automated moves have no predicting player to refresh");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cloningCannotCreateAdditionalEggCopies() {
|
||||
var clone = click(30, ClickType.MIDDLE, InventoryAction.CLONE_STACK, -1);
|
||||
listener.onInventoryClick(clone);
|
||||
assertTrue(clone.isCancelled());
|
||||
assertEquals(1, scheduled.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinaryItemsRemainUnaffectedWithoutRefreshTraffic() {
|
||||
ItemStack ordinary = mock(ItemStack.class);
|
||||
when(ordinary.clone()).thenReturn(ordinary);
|
||||
contents[3] = ordinary;
|
||||
var shift = click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
listener.onInventoryClick(shift);
|
||||
assertFalse(shift.isCancelled());
|
||||
var drag = new org.bukkit.event.inventory.InventoryDragEvent(view, null, ordinary, false, Map.of(0, ordinary));
|
||||
listener.onInventoryDrag(drag);
|
||||
assertFalse(drag.isCancelled());
|
||||
var hopper = new org.bukkit.event.inventory.InventoryMoveItemEvent(top, ordinary, mock(Inventory.class), true);
|
||||
listener.onInventoryMove(hopper);
|
||||
assertFalse(hopper.isCancelled());
|
||||
assertTrue(scheduled.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void safeInternalCursorMovementDoesNotTriggerRejectionOrRefresh() {
|
||||
var pickup = click(30, ClickType.LEFT, InventoryAction.PICKUP_ALL, -1);
|
||||
listener.onInventoryClick(pickup);
|
||||
assertFalse(pickup.isCancelled());
|
||||
contents[3] = null;
|
||||
cursor.set(egg);
|
||||
var place = click(31, ClickType.LEFT, InventoryAction.PLACE_ALL, -1);
|
||||
listener.onInventoryClick(place);
|
||||
assertFalse(place.isCancelled());
|
||||
var drag = new org.bukkit.event.inventory.InventoryDragEvent(view, null, egg, false, Map.of(31, egg));
|
||||
listener.onInventoryDrag(drag);
|
||||
assertFalse(drag.isCancelled());
|
||||
assertTrue(scheduled.isEmpty());
|
||||
new CapturedMobInventoryService(items, PluginSettings.from(Map.of()))
|
||||
.reconcile(player, manager.player(owner, "Tamer"));
|
||||
assertSame(egg, cursor.get());
|
||||
verify(inventory, never()).addItem(any(ItemStack[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cursorTransferAndExistingCancellationPreserveCustodyAndResynchronize() throws Exception {
|
||||
var before = manager.snapshot();
|
||||
byte[] bytes = Files.readAllBytes(stateFile);
|
||||
contents[3] = null;
|
||||
cursor.set(egg);
|
||||
var external = click(0, ClickType.LEFT, InventoryAction.PLACE_ALL, -1);
|
||||
listener.onInventoryClick(external);
|
||||
assertTrue(external.isCancelled());
|
||||
var cancelled = click(31, ClickType.LEFT, InventoryAction.PLACE_ALL, -1);
|
||||
cancelled.setCancelled(true);
|
||||
listener.onInventoryClick(cancelled);
|
||||
assertTrue(cancelled.isCancelled());
|
||||
assertEquals(1, scheduled.size());
|
||||
scheduled.remove(0).run();
|
||||
assertSame(egg, cursor.get());
|
||||
assertEquals(before, manager.snapshot());
|
||||
assertArrayEquals(bytes, Files.readAllBytes(stateFile));
|
||||
verify(player).updateInventory();
|
||||
verify(top, never()).setItem(anyInt(), nullable(ItemStack.class));
|
||||
}
|
||||
|
||||
private InventoryClickEvent click(int raw, ClickType click, InventoryAction action, int button) {
|
||||
return new InventoryClickEvent(view, InventoryType.SlotType.CONTAINER, raw, click, action, button);
|
||||
}
|
||||
|
||||
private ItemStack egg(UUID id, UUID captured) {
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
markEgg(item, id, captured);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void markEgg(ItemStack item, UUID id, UUID captured) {
|
||||
ItemMeta meta = mock(ItemMeta.class);
|
||||
PersistentDataContainer data = mock(PersistentDataContainer.class);
|
||||
when(item.clone()).thenReturn(item);
|
||||
when(item.hasItemMeta()).thenReturn(true);
|
||||
when(item.getItemMeta()).thenReturn(meta);
|
||||
var amount = new java.util.concurrent.atomic.AtomicInteger(1);
|
||||
when(item.getAmount()).thenAnswer(call -> amount.get());
|
||||
doAnswer(call -> { amount.set(call.getArgument(0)); return null; }).when(item).setAmount(anyInt());
|
||||
when(meta.getPersistentDataContainer()).thenReturn(data);
|
||||
when(data.get(new NamespacedKey("spigottyrant", "captured-owner"), PersistentDataType.STRING))
|
||||
.thenReturn(id.toString());
|
||||
when(data.get(new NamespacedKey("spigottyrant", "captured-id"), PersistentDataType.STRING))
|
||||
.thenReturn(captured.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarFile;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
class PurpurDistributionTest {
|
||||
@Test
|
||||
void packagesNewNameWithoutChangingRuntimeIdentityOrBundlingTheApi() throws Exception {
|
||||
Path artifact = Path.of(System.getProperty("distribution.jar"));
|
||||
String version = System.getProperty("distribution.version");
|
||||
assertEquals("purpur-tyrant-" + version + ".jar", artifact.getFileName().toString());
|
||||
try (var jar = new JarFile(artifact.toFile())) {
|
||||
Map<?, ?> metadata = new Yaml().load(jar.getInputStream(jar.getJarEntry("plugin.yml")));
|
||||
assertEquals(version, metadata.get("version"));
|
||||
assertEquals("SpigotTyrant", metadata.get("name"), "Retain the data directory and persistent namespace");
|
||||
assertEquals("games.dmg.spigottyrant.SpigotTyrantPlugin", metadata.get("main"));
|
||||
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigottyrant.admin"));
|
||||
assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/")
|
||||
|| entry.getName().startsWith("org/purpurmc/") || entry.getName().endsWith("Test.class")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void pluginBytecodeTargetsJava25() throws Exception {
|
||||
try (var jar = new JarFile(System.getProperty("distribution.jar"));
|
||||
var bytecode = new DataInputStream(jar.getInputStream(
|
||||
jar.getJarEntry("games/dmg/spigottyrant/SpigotTyrantPlugin.class")))) {
|
||||
assertEquals(0xCAFEBABE, bytecode.readInt());
|
||||
bytecode.readUnsignedShort();
|
||||
assertEquals(69, bytecode.readUnsignedShort());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() throws Exception {
|
||||
String build = Files.readString(Path.of("build.gradle.kts"));
|
||||
assertTrue(build.contains("purpur-api:26.2.build.2618-stable"));
|
||||
assertFalse(build.contains("org.spigotmc:spigot-api"));
|
||||
for (String workflow : new String[] {"ci.yml", "release.yml"}) {
|
||||
String content = Files.readString(Path.of(".gitea/workflows", workflow));
|
||||
assertTrue(content.contains("java-version: 25"));
|
||||
assertTrue(content.contains("purpur-tyrant-"), workflow);
|
||||
assertFalse(content.contains("spigot-tyrant-"), workflow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ final class RoleArenaLifecycleTest {
|
||||
void vacantArenaWithoutNearbyPlayersDoesNotSpawnABoss() {
|
||||
Fixture f = new Fixture();
|
||||
f.controller.run();
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@@ -39,7 +39,7 @@ final class RoleArenaLifecycleTest {
|
||||
}
|
||||
Player player = f.player(18.01, 64, 0);
|
||||
f.controller.run();
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any());
|
||||
when(player.getLocation()).thenReturn(new Location(f.world, 18, 64, 0));
|
||||
f.controller.run();
|
||||
f.controller.run();
|
||||
@@ -61,11 +61,11 @@ final class RoleArenaLifecycleTest {
|
||||
when(f.world.isChunkLoaded(0, 0)).thenReturn(false);
|
||||
f.controller.run();
|
||||
verify(f.world, never()).getChunkAt(0, 0);
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any());
|
||||
when(f.world.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
when(f.chunk.isEntitiesLoaded()).thenReturn(false);
|
||||
f.controller.run();
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
verify(f.world, never()).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any());
|
||||
when(f.chunk.isEntitiesLoaded()).thenReturn(true);
|
||||
f.controller.run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, f.spawned.size());
|
||||
@@ -132,7 +132,7 @@ final class RoleArenaLifecycleTest {
|
||||
f.player(15, 64, 0);
|
||||
Zombie rejected = f.taggedBoss();
|
||||
f.entities.remove(rejected.getUniqueId());
|
||||
when(f.world.spawn(any(Location.class), eq(Zombie.class), any())).thenReturn(rejected);
|
||||
when(f.world.spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any())).thenReturn(rejected);
|
||||
f.controller.run();
|
||||
verify(rejected).remove();
|
||||
verify(f.world, never()).spawnParticle(eq(org.bukkit.Particle.PORTAL), any(Location.class),
|
||||
@@ -236,13 +236,13 @@ final class RoleArenaLifecycleTest {
|
||||
void failedSpawnLeavesNoOwnershipAndBacksOffBeforeRetry() {
|
||||
Fixture f = new Fixture();
|
||||
f.player(15, 64, 0);
|
||||
when(f.world.spawn(any(Location.class), eq(Zombie.class), any()))
|
||||
when(f.world.spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any()))
|
||||
.thenThrow(new IllegalStateException("spawn rejected"));
|
||||
org.junit.jupiter.api.Assertions.assertDoesNotThrow(f.controller::run);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
f.controller.run();
|
||||
}
|
||||
verify(f.world).spawn(any(Location.class), eq(Zombie.class), any());
|
||||
verify(f.world).spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(0, f.entities.size());
|
||||
f.controller.close();
|
||||
}
|
||||
@@ -385,7 +385,7 @@ final class RoleArenaLifecycleTest {
|
||||
PlayerState.newPlayer(player.getUniqueId(), "Visitor")));
|
||||
return states;
|
||||
});
|
||||
when(world.spawn(any(Location.class), eq(Zombie.class), any())).thenAnswer(call -> {
|
||||
when(world.spawn(any(Location.class), eq(Zombie.class), org.mockito.ArgumentMatchers.<java.util.function.Consumer<Zombie>>any())).thenAnswer(call -> {
|
||||
Zombie boss = newZombie();
|
||||
spawned.add(boss);
|
||||
java.util.function.Consumer<Zombie> initializer = call.getArgument(2);
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryAction;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryView;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
class RoleControlInventoryMovementTest {
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void ownerCanShiftBetweenOwnInventorySections(RoleControl role) {
|
||||
Fixture f = new Fixture(role, true);
|
||||
f.slots.put(10, f.bound(f.owner, role));
|
||||
var event = f.click(10, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
f.listener.onInventoryClick(event);
|
||||
assertFalse(event.isCancelled(), "Personal inventory shift stays in owner inventory");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void ownerCanDragAcrossOnlyTheirInventorySlots(RoleControl role) {
|
||||
Fixture f = new Fixture(role, false);
|
||||
ItemStack item = f.bound(f.owner, role);
|
||||
var event = new InventoryDragEvent(f.view, null, item, false, Map.of(30, item));
|
||||
f.listener.onInventoryDrag(event);
|
||||
assertFalse(event.isCancelled(), "An open chest must not prevent safe bottom-inventory drag");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void boundHotbarSourceCannotBeSwappedIntoAContainer(RoleControl role) {
|
||||
Fixture f = new Fixture(role, false);
|
||||
ItemStack source = f.bound(f.owner, role);
|
||||
when(f.inventory.getItem(2)).thenReturn(source);
|
||||
f.slots.put(0, mock(ItemStack.class));
|
||||
var event = f.click(0, ClickType.NUMBER_KEY, InventoryAction.HOTBAR_SWAP, 2);
|
||||
f.listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled(), "Inspect the hotbar source, not only clicked item/cursor");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void boundOffhandSourceCannotBeSwappedIntoAContainer(RoleControl role) {
|
||||
Fixture f = new Fixture(role, false);
|
||||
ItemStack source = f.bound(f.owner, role);
|
||||
when(f.inventory.getItemInOffHand()).thenReturn(source);
|
||||
var event = f.click(0, ClickType.SWAP_OFFHAND, InventoryAction.HOTBAR_SWAP, -1);
|
||||
f.listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void creativeCloneDoesNotCreateTransferableOrExtraRoleItems(RoleControl role) {
|
||||
Fixture f = new Fixture(role, true);
|
||||
f.slots.put(10, f.bound(f.owner, role));
|
||||
var event = f.click(10, ClickType.MIDDLE, InventoryAction.CLONE_STACK, -1);
|
||||
f.listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void ordinaryCursorMovesAndInternalHotbarSwapsStayAllowed(RoleControl role) {
|
||||
Fixture f = new Fixture(role, false);
|
||||
ItemStack bound = f.bound(f.owner, role);
|
||||
f.slots.put(30, bound);
|
||||
var pickup = f.click(30, ClickType.LEFT, InventoryAction.PICKUP_ALL, -1);
|
||||
f.listener.onInventoryClick(pickup);
|
||||
assertFalse(pickup.isCancelled());
|
||||
f.slots.remove(30);
|
||||
f.cursor.set(bound);
|
||||
var place = f.click(31, ClickType.RIGHT, InventoryAction.PLACE_ONE, -1);
|
||||
f.listener.onInventoryClick(place);
|
||||
assertFalse(place.isCancelled());
|
||||
f.cursor.set(null);
|
||||
when(f.inventory.getItem(2)).thenReturn(bound);
|
||||
var swap = f.click(32, ClickType.NUMBER_KEY, InventoryAction.HOTBAR_SWAP, 2);
|
||||
f.listener.onInventoryClick(swap);
|
||||
assertFalse(swap.isCancelled());
|
||||
when(f.inventory.getItemInOffHand()).thenReturn(bound);
|
||||
var offhand = f.click(32, ClickType.SWAP_OFFHAND, InventoryAction.HOTBAR_SWAP, -1);
|
||||
f.listener.onInventoryClick(offhand);
|
||||
assertFalse(offhand.isCancelled());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void crossInventoryShiftsDragsAndCollectionAreBlocked(RoleControl role) {
|
||||
Fixture f = new Fixture(role, false);
|
||||
ItemStack bound = f.bound(f.owner, role);
|
||||
f.slots.put(30, bound);
|
||||
var shift = f.click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
f.listener.onInventoryClick(shift);
|
||||
assertTrue(shift.isCancelled());
|
||||
f.cursor.set(bound);
|
||||
var place = f.click(0, ClickType.LEFT, InventoryAction.PLACE_ALL, -1);
|
||||
f.listener.onInventoryClick(place);
|
||||
assertTrue(place.isCancelled());
|
||||
var drag = new InventoryDragEvent(f.view, null, bound, false, Map.of(30, bound, 0, bound));
|
||||
f.listener.onInventoryDrag(drag);
|
||||
assertTrue(drag.isCancelled());
|
||||
var collect = f.click(30, ClickType.DOUBLE_CLICK, InventoryAction.COLLECT_TO_CURSOR, -1);
|
||||
f.listener.onInventoryClick(collect);
|
||||
assertTrue(collect.isCancelled(), "Collection also scans the external inventory");
|
||||
var outside = f.click(-999, ClickType.LEFT, InventoryAction.DROP_ALL_CURSOR, -1);
|
||||
f.listener.onInventoryClick(outside);
|
||||
assertTrue(outside.isCancelled());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void foreignControlItemsCannotBeRetainedThroughMovement(RoleControl role) {
|
||||
Fixture f = new Fixture(role, true);
|
||||
ItemStack foreign = f.bound(UUID.randomUUID(), role);
|
||||
f.slots.put(10, foreign);
|
||||
var pickup = f.click(10, ClickType.LEFT, InventoryAction.PICKUP_ALL, -1);
|
||||
f.listener.onInventoryClick(pickup);
|
||||
assertTrue(pickup.isCancelled());
|
||||
f.slots.clear();
|
||||
when(f.inventory.getItem(2)).thenReturn(foreign);
|
||||
var swap = f.click(10, ClickType.NUMBER_KEY, InventoryAction.HOTBAR_SWAP, 2);
|
||||
f.listener.onInventoryClick(swap);
|
||||
assertTrue(swap.isCancelled());
|
||||
var drag = new InventoryDragEvent(f.view, null, foreign, false, Map.of(10, foreign));
|
||||
f.listener.onInventoryDrag(drag);
|
||||
assertTrue(drag.isCancelled());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void craftingGridAndWorkbenchRemainExternalDestinations(RoleControl role) {
|
||||
Fixture own = new Fixture(role, true);
|
||||
own.cursor.set(own.bound(own.owner, role));
|
||||
var grid = own.click(1, ClickType.LEFT, InventoryAction.PLACE_ALL, -1);
|
||||
own.listener.onInventoryClick(grid);
|
||||
assertTrue(grid.isCancelled());
|
||||
Fixture bench = new Fixture(role, 9);
|
||||
bench.slots.put(15, bench.bound(bench.owner, role));
|
||||
var shift = bench.click(15, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
bench.listener.onInventoryClick(shift);
|
||||
assertTrue(shift.isCancelled(), "Only the personal 2x2 view has internal shift routing");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(RoleControl.class)
|
||||
void cancelledSafeMovesStayCancelledAndOrdinaryItemsAreUnaffected(RoleControl role) {
|
||||
Fixture f = new Fixture(role, true);
|
||||
f.slots.put(10, f.bound(f.owner, role));
|
||||
var event = f.click(10, ClickType.LEFT, InventoryAction.PICKUP_ALL, -1);
|
||||
event.setCancelled(true);
|
||||
f.listener.onInventoryClick(event);
|
||||
assertTrue(event.isCancelled());
|
||||
Fixture chest = new Fixture(role, false);
|
||||
ItemStack ordinary = mock(ItemStack.class);
|
||||
when(ordinary.clone()).thenReturn(ordinary);
|
||||
chest.slots.put(30, ordinary);
|
||||
var shift = chest.click(30, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY, -1);
|
||||
chest.listener.onInventoryClick(shift);
|
||||
assertFalse(shift.isCancelled());
|
||||
var drag = new InventoryDragEvent(chest.view, null, ordinary, false, Map.of(0, ordinary));
|
||||
chest.listener.onInventoryDrag(drag);
|
||||
assertFalse(drag.isCancelled());
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final UUID owner = UUID.randomUUID();
|
||||
private final Player player = mock(Player.class);
|
||||
private final PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
private final InventoryView view = mock(InventoryView.class);
|
||||
private final Map<Integer, ItemStack> slots = new HashMap<>();
|
||||
private final AtomicReference<ItemStack> cursor = new AtomicReference<>();
|
||||
private final RoleControlItemListener listener;
|
||||
|
||||
Fixture(RoleControl role, boolean personalCrafting) {
|
||||
this(role, personalCrafting ? 4 : 0);
|
||||
}
|
||||
|
||||
Fixture(RoleControl role, int matrixSize) {
|
||||
int topSize = matrixSize > 0 ? matrixSize + 1 : 27;
|
||||
when(player.getUniqueId()).thenReturn(owner);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(player.getItemOnCursor()).thenAnswer(call -> cursor.get());
|
||||
when(view.getPlayer()).thenReturn(player);
|
||||
when(view.getBottomInventory()).thenReturn(inventory);
|
||||
Inventory top;
|
||||
if (matrixSize > 0) {
|
||||
var crafting = mock(org.bukkit.inventory.CraftingInventory.class);
|
||||
when(crafting.getMatrix()).thenReturn(new ItemStack[matrixSize]);
|
||||
top = crafting;
|
||||
} else {
|
||||
top = mock(Inventory.class);
|
||||
}
|
||||
when(top.getSize()).thenReturn(topSize);
|
||||
when(view.getTopInventory()).thenReturn(top);
|
||||
when(view.getInventory(anyInt())).thenAnswer(call -> {
|
||||
int raw = call.getArgument(0);
|
||||
return raw < 0 || raw >= topSize + 41 ? null : raw < topSize ? top : inventory;
|
||||
});
|
||||
when(view.convertSlot(anyInt())).thenAnswer(call -> call.getArgument(0, Integer.class) - topSize);
|
||||
when(view.getItem(anyInt())).thenAnswer(call -> slots.get(call.getArgument(0)));
|
||||
when(view.getCursor()).thenAnswer(call -> cursor.get());
|
||||
Plugin plugin = mock(Plugin.class);
|
||||
when(plugin.getName()).thenReturn("SpigotTyrant");
|
||||
when(plugin.namespace()).thenReturn("spigottyrant");
|
||||
var items = new BukkitRoleControlItemService(plugin, PluginSettings.from(Map.of()));
|
||||
var manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(new GameState(GameLifecycle.RUNNING,
|
||||
role == RoleControl.TYRANT ? Optional.of(owner) : Optional.empty(),
|
||||
role == RoleControl.VIGILANTE ? Optional.of(owner) : Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO, 0, 0, Set.of()));
|
||||
listener = new RoleControlItemListener(manager, items, mock(TyrantControlPanel.class),
|
||||
mock(VigilanteControlPanel.class));
|
||||
}
|
||||
|
||||
InventoryClickEvent click(int raw, ClickType click, InventoryAction action, int hotbar) {
|
||||
return new InventoryClickEvent(view, InventoryType.SlotType.CONTAINER, raw, click, action, hotbar);
|
||||
}
|
||||
|
||||
ItemStack bound(UUID id, RoleControl role) {
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
ItemMeta meta = mock(ItemMeta.class);
|
||||
PersistentDataContainer data = mock(PersistentDataContainer.class);
|
||||
when(item.clone()).thenReturn(item);
|
||||
when(item.hasItemMeta()).thenReturn(true);
|
||||
when(item.getItemMeta()).thenReturn(meta);
|
||||
when(meta.getPersistentDataContainer()).thenReturn(data);
|
||||
when(data.get(new NamespacedKey("spigottyrant", "role-control"), PersistentDataType.STRING))
|
||||
.thenReturn(role.name());
|
||||
when(data.get(new NamespacedKey("spigottyrant", "role-control-owner"), PersistentDataType.STRING))
|
||||
.thenReturn(id.toString());
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ final class TamerListenerTest {
|
||||
CapturedMobItemService capturedItems = mock(CapturedMobItemService.class);
|
||||
TamerListener listener = new TamerListener(
|
||||
manager, abilityItems, capturedItems, new CapturedMobService(),
|
||||
PluginSettings.from(Map.of()), mock(Server.class)
|
||||
PluginSettings.from(Map.of()), mock(Server.class), Runnable::run
|
||||
);
|
||||
|
||||
listener.onCapture(event);
|
||||
|
||||
Reference in New Issue
Block a user