Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

InvSwitcher is a BentoBox addon for Minecraft (Spigot/Bukkit) that gives players separate inventories, ender chests, health, food, experience, advancements, game modes, and statistics per game-world. Nether and End dimensions are automatically grouped with their overworld.

## Build Commands

- **Build**: `mvn clean package`
- **Run tests**: `mvn test -Dmaven.compiler.forceJavacCompilerUse=true`
- **Run a single test class**: `mvn test -Dmaven.compiler.forceJavacCompilerUse=true -Dtest=StoreTest`
- **Run a single test method**: `mvn test -Dmaven.compiler.forceJavacCompilerUse=true -Dtest=StoreTest#testMethodName`

Requires Java 21. The build produces a shaded JAR in `target/`. The `-Dmaven.compiler.forceJavacCompilerUse=true` flag is needed to work around a compiler hashing bug with the current JDK.

## Architecture

This is a BentoBox Addon (extends `Addon`, not a standalone Bukkit plugin). Key flow:

- **InvSwitcher** - Addon entry point. Loads config, resolves configured world names to `World` objects (including nether/end variants), creates the `Store`, and registers `PlayerListener`.
- **Store** - Core logic. Maintains an in-memory `Map<UUID, InventoryStorage>` cache backed by BentoBox's `Database`. On world change: stores current player state to the old world's slot, clears the player, then loads the new world's slot. Handles XP math manually (Bukkit's `getTotalExperience()` is unreliable). Tracks per-player `currentKey` to map saves/loads to the correct storage slot.
- **InventoryStorage** - `DataObject` (BentoBox DB entity) keyed by player UUID. All per-world data is stored as `Map<String, ...>` where the key is either the overworld name (e.g., `"oneblock_world"`) or an island-specific key (e.g., `"oneblock_world/islandId"`). Persisted via BentoBox's database abstraction (JSON, MySQL, etc. — **not** YAML, which is explicitly unsupported).
- **PlayerListener** - Listens to `PlayerChangedWorldEvent`, `PlayerJoinEvent`, `PlayerQuitEvent`, `IslandEnterEvent`, and `PlayerRespawnEvent` to trigger store/load operations.
- **Settings** - `ConfigObject` loaded from `config.yml`. Each switchable aspect (inventory, health, food, etc.) has a world-level boolean toggle and a per-island sub-toggle.

## Key Design Details

- World name normalization: nether (`_nether`) and end (`_the_end`) suffixes are stripped to map all three dimensions to the same overworld key.
- **Per-island inventory switching**: When `islandsActive` is enabled and a player owns multiple concurrent islands, data is keyed as `"worldName/islandId"` instead of just `"worldName"`. Each data type (inventory, health, etc.) has its own per-island sub-toggle.
- **Storage key transitions**: When a player goes from 1 island to multiple, their `currentKey` must be upgraded from a world-only key to an island-specific key. This is handled proactively in `PlayerListener.onIslandEnter` via `Store.upgradeWorldKeyToIsland()` before any save/load occurs.
- **Backward compatibility migration**: `Store.getInventory()` migrates world-only data to island-specific keys on first load if no island-specific data exists yet.
- Statistics saving runs asynchronously via `Bukkit.getScheduler()` except during shutdown (where scheduling is unavailable).
- Tests use JUnit 5 + MockBukkit + Mockito. The surefire plugin requires extensive `--add-opens` flags (already configured in pom.xml).
63 changes: 52 additions & 11 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<!-- Non-minecraft related dependencies -->
<mockito.version>5.12.0</mockito.version>
<mockito.version>5.17.0</mockito.version>
<junit.version>5.12.1</junit.version>
<byte-buddy.version>1.17.5</byte-buddy.version>
<!-- More visible way how to change dependency versions -->
<spigot.version>1.21.3-R0.1-SNAPSHOT</spigot.version>
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
<bentobox.version>2.7.1-SNAPSHOT</bentobox.version>
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
Expand Down Expand Up @@ -112,6 +114,14 @@
</profiles>

<repositories>
<!-- jitpack first so MockBukkit snapshots resolve without hitting other repos -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots</url>
Expand All @@ -120,16 +130,40 @@
<id>codemc</id>
<url>https://repo.codemc.org/repository/bentoboxworld/</url>
</repository>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>

<dependencies>
<!-- Spigot API -->
<!-- Paper API -->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>${spigot.version}</version>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${paper.version}</version>
<scope>provided</scope>
</dependency>
<!-- MockBukkit -->
<dependency>
<groupId>com.github.MockBukkit</groupId>
<artifactId>MockBukkit</artifactId>
<version>v1.21-SNAPSHOT</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Mockito (Unit testing) -->
<dependency>
<groupId>org.mockito</groupId>
Expand All @@ -139,14 +173,21 @@
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.0.0</version>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- ByteBuddy - explicit version for Java 25 support -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte-buddy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>${byte-buddy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
83 changes: 82 additions & 1 deletion src/main/java/com/wasteofplastic/invswitcher/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ public class Settings implements ConfigObject {
@ConfigEntry(path = "options.statistics")
private boolean statistics = true;

@ConfigComment("Switch inventories based on island. Only applies if players own more than one island.")
@ConfigComment("Each sub-option controls whether that aspect is switched per-island.")
@ConfigComment("The world-level option must also be true for the island option to have any effect.")
@ConfigEntry(path = "options.islands.active")
private boolean islandsActive = true;
@ConfigEntry(path = "options.islands.inventory")
private boolean islandsInventory = true;
@ConfigEntry(path = "options.islands.health")
private boolean islandsHealth = false;
@ConfigEntry(path = "options.islands.food")
private boolean islandsFood = false;
@ConfigEntry(path = "options.islands.advancements")
private boolean islandsAdvancements = false;
@ConfigEntry(path = "options.islands.gamemode")
private boolean islandsGamemode = false;
@ConfigEntry(path = "options.islands.experience")
private boolean islandsExperience = false;
@ConfigEntry(path = "options.islands.ender-chest")
private boolean islandsEnderChest = true;
@ConfigEntry(path = "options.islands.statistics")
private boolean islandsStatistics = false;

/**
* @return the worlds
*/
Expand Down Expand Up @@ -143,6 +165,65 @@ public boolean isStatistics() {
public void setStatistics(boolean statistics) {
this.statistics = statistics;
}

/**
* @return whether per-island switching is active
*/
public boolean isIslandsActive() {
return islandsActive;
}
/**
* @param islandsActive whether to enable per-island switching
*/
public void setIslandsActive(boolean islandsActive) {
this.islandsActive = islandsActive;
}
public boolean isIslandsInventory() {
return islandsInventory;
}
public void setIslandsInventory(boolean islandsInventory) {
this.islandsInventory = islandsInventory;
}
public boolean isIslandsHealth() {
return islandsHealth;
}
public void setIslandsHealth(boolean islandsHealth) {
this.islandsHealth = islandsHealth;
}
public boolean isIslandsFood() {
return islandsFood;
}
public void setIslandsFood(boolean islandsFood) {
this.islandsFood = islandsFood;
}
public boolean isIslandsAdvancements() {
return islandsAdvancements;
}
public void setIslandsAdvancements(boolean islandsAdvancements) {
this.islandsAdvancements = islandsAdvancements;
}
public boolean isIslandsGamemode() {
return islandsGamemode;
}
public void setIslandsGamemode(boolean islandsGamemode) {
this.islandsGamemode = islandsGamemode;
}
public boolean isIslandsExperience() {
return islandsExperience;
}
public void setIslandsExperience(boolean islandsExperience) {
this.islandsExperience = islandsExperience;
}
public boolean isIslandsEnderChest() {
return islandsEnderChest;
}
public void setIslandsEnderChest(boolean islandsEnderChest) {
this.islandsEnderChest = islandsEnderChest;
}
public boolean isIslandsStatistics() {
return islandsStatistics;
}
public void setIslandsStatistics(boolean islandsStatistics) {
this.islandsStatistics = islandsStatistics;
}

}
Loading
Loading