Skip to content

Commit 97bf247

Browse files
authored
Merge pull request #42 from BentoBoxWorld/develop
Fix inventory lost when returning to original island
2 parents c3e931c + 27e1833 commit 97bf247

12 files changed

Lines changed: 1179 additions & 300 deletions

File tree

CLAUDE.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
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.
8+
9+
## Build Commands
10+
11+
- **Build**: `mvn clean package`
12+
- **Run tests**: `mvn test -Dmaven.compiler.forceJavacCompilerUse=true`
13+
- **Run a single test class**: `mvn test -Dmaven.compiler.forceJavacCompilerUse=true -Dtest=StoreTest`
14+
- **Run a single test method**: `mvn test -Dmaven.compiler.forceJavacCompilerUse=true -Dtest=StoreTest#testMethodName`
15+
16+
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.
17+
18+
## Architecture
19+
20+
This is a BentoBox Addon (extends `Addon`, not a standalone Bukkit plugin). Key flow:
21+
22+
- **InvSwitcher** - Addon entry point. Loads config, resolves configured world names to `World` objects (including nether/end variants), creates the `Store`, and registers `PlayerListener`.
23+
- **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.
24+
- **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).
25+
- **PlayerListener** - Listens to `PlayerChangedWorldEvent`, `PlayerJoinEvent`, `PlayerQuitEvent`, `IslandEnterEvent`, and `PlayerRespawnEvent` to trigger store/load operations.
26+
- **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.
27+
28+
## Key Design Details
29+
30+
- World name normalization: nether (`_nether`) and end (`_the_end`) suffixes are stripped to map all three dimensions to the same overworld key.
31+
- **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.
32+
- **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.
33+
- **Backward compatibility migration**: `Store.getInventory()` migrates world-only data to island-specific keys on first load if no island-specific data exists yet.
34+
- Statistics saving runs asynchronously via `Bukkit.getScheduler()` except during shutdown (where scheduling is unavailable).
35+
- Tests use JUnit 5 + MockBukkit + Mockito. The surefire plugin requires extensive `--add-opens` flags (already configured in pom.xml).

pom.xml

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@
5353
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
5454
<java.version>21</java.version>
5555
<!-- Non-minecraft related dependencies -->
56-
<mockito.version>5.12.0</mockito.version>
56+
<mockito.version>5.17.0</mockito.version>
57+
<junit.version>5.12.1</junit.version>
58+
<byte-buddy.version>1.17.5</byte-buddy.version>
5759
<!-- More visible way how to change dependency versions -->
58-
<spigot.version>1.21.3-R0.1-SNAPSHOT</spigot.version>
60+
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
5961
<bentobox.version>2.7.1-SNAPSHOT</bentobox.version>
6062
<!-- Revision variable removes warning about dynamic version -->
6163
<revision>${build.version}-SNAPSHOT</revision>
@@ -112,6 +114,14 @@
112114
</profiles>
113115

114116
<repositories>
117+
<!-- jitpack first so MockBukkit snapshots resolve without hitting other repos -->
118+
<repository>
119+
<id>jitpack.io</id>
120+
<url>https://jitpack.io</url>
121+
<snapshots>
122+
<enabled>true</enabled>
123+
</snapshots>
124+
</repository>
115125
<repository>
116126
<id>spigot-repo</id>
117127
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots</url>
@@ -120,16 +130,40 @@
120130
<id>codemc</id>
121131
<url>https://repo.codemc.org/repository/bentoboxworld/</url>
122132
</repository>
133+
<repository>
134+
<id>papermc</id>
135+
<url>https://repo.papermc.io/repository/maven-public/</url>
136+
</repository>
123137
</repositories>
124138

125139
<dependencies>
126-
<!-- Spigot API -->
140+
<!-- Paper API -->
127141
<dependency>
128-
<groupId>org.spigotmc</groupId>
129-
<artifactId>spigot-api</artifactId>
130-
<version>${spigot.version}</version>
142+
<groupId>io.papermc.paper</groupId>
143+
<artifactId>paper-api</artifactId>
144+
<version>${paper.version}</version>
131145
<scope>provided</scope>
132146
</dependency>
147+
<!-- MockBukkit -->
148+
<dependency>
149+
<groupId>com.github.MockBukkit</groupId>
150+
<artifactId>MockBukkit</artifactId>
151+
<version>v1.21-SNAPSHOT</version>
152+
<scope>test</scope>
153+
<exclusions>
154+
<exclusion>
155+
<groupId>org.junit.jupiter</groupId>
156+
<artifactId>junit-jupiter-api</artifactId>
157+
</exclusion>
158+
</exclusions>
159+
</dependency>
160+
<!-- JUnit 5 -->
161+
<dependency>
162+
<groupId>org.junit.jupiter</groupId>
163+
<artifactId>junit-jupiter</artifactId>
164+
<version>${junit.version}</version>
165+
<scope>test</scope>
166+
</dependency>
133167
<!-- Mockito (Unit testing) -->
134168
<dependency>
135169
<groupId>org.mockito</groupId>
@@ -139,14 +173,21 @@
139173
</dependency>
140174
<dependency>
141175
<groupId>org.mockito</groupId>
142-
<artifactId>mockito-inline</artifactId>
143-
<version>5.0.0</version>
176+
<artifactId>mockito-junit-jupiter</artifactId>
177+
<version>${mockito.version}</version>
178+
<scope>test</scope>
179+
</dependency>
180+
<!-- ByteBuddy - explicit version for Java 25 support -->
181+
<dependency>
182+
<groupId>net.bytebuddy</groupId>
183+
<artifactId>byte-buddy</artifactId>
184+
<version>${byte-buddy.version}</version>
144185
<scope>test</scope>
145186
</dependency>
146187
<dependency>
147-
<groupId>junit</groupId>
148-
<artifactId>junit</artifactId>
149-
<version>4.13.2</version>
188+
<groupId>net.bytebuddy</groupId>
189+
<artifactId>byte-buddy-agent</artifactId>
190+
<version>${byte-buddy.version}</version>
150191
<scope>test</scope>
151192
</dependency>
152193
<dependency>

src/main/java/com/wasteofplastic/invswitcher/Settings.java

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ public class Settings implements ConfigObject {
3535
@ConfigEntry(path = "options.statistics")
3636
private boolean statistics = true;
3737

38+
@ConfigComment("Switch inventories based on island. Only applies if players own more than one island.")
39+
@ConfigComment("Each sub-option controls whether that aspect is switched per-island.")
40+
@ConfigComment("The world-level option must also be true for the island option to have any effect.")
41+
@ConfigEntry(path = "options.islands.active")
42+
private boolean islandsActive = true;
43+
@ConfigEntry(path = "options.islands.inventory")
44+
private boolean islandsInventory = true;
45+
@ConfigEntry(path = "options.islands.health")
46+
private boolean islandsHealth = false;
47+
@ConfigEntry(path = "options.islands.food")
48+
private boolean islandsFood = false;
49+
@ConfigEntry(path = "options.islands.advancements")
50+
private boolean islandsAdvancements = false;
51+
@ConfigEntry(path = "options.islands.gamemode")
52+
private boolean islandsGamemode = false;
53+
@ConfigEntry(path = "options.islands.experience")
54+
private boolean islandsExperience = false;
55+
@ConfigEntry(path = "options.islands.ender-chest")
56+
private boolean islandsEnderChest = true;
57+
@ConfigEntry(path = "options.islands.statistics")
58+
private boolean islandsStatistics = false;
59+
3860
/**
3961
* @return the worlds
4062
*/
@@ -143,6 +165,65 @@ public boolean isStatistics() {
143165
public void setStatistics(boolean statistics) {
144166
this.statistics = statistics;
145167
}
146-
168+
/**
169+
* @return whether per-island switching is active
170+
*/
171+
public boolean isIslandsActive() {
172+
return islandsActive;
173+
}
174+
/**
175+
* @param islandsActive whether to enable per-island switching
176+
*/
177+
public void setIslandsActive(boolean islandsActive) {
178+
this.islandsActive = islandsActive;
179+
}
180+
public boolean isIslandsInventory() {
181+
return islandsInventory;
182+
}
183+
public void setIslandsInventory(boolean islandsInventory) {
184+
this.islandsInventory = islandsInventory;
185+
}
186+
public boolean isIslandsHealth() {
187+
return islandsHealth;
188+
}
189+
public void setIslandsHealth(boolean islandsHealth) {
190+
this.islandsHealth = islandsHealth;
191+
}
192+
public boolean isIslandsFood() {
193+
return islandsFood;
194+
}
195+
public void setIslandsFood(boolean islandsFood) {
196+
this.islandsFood = islandsFood;
197+
}
198+
public boolean isIslandsAdvancements() {
199+
return islandsAdvancements;
200+
}
201+
public void setIslandsAdvancements(boolean islandsAdvancements) {
202+
this.islandsAdvancements = islandsAdvancements;
203+
}
204+
public boolean isIslandsGamemode() {
205+
return islandsGamemode;
206+
}
207+
public void setIslandsGamemode(boolean islandsGamemode) {
208+
this.islandsGamemode = islandsGamemode;
209+
}
210+
public boolean isIslandsExperience() {
211+
return islandsExperience;
212+
}
213+
public void setIslandsExperience(boolean islandsExperience) {
214+
this.islandsExperience = islandsExperience;
215+
}
216+
public boolean isIslandsEnderChest() {
217+
return islandsEnderChest;
218+
}
219+
public void setIslandsEnderChest(boolean islandsEnderChest) {
220+
this.islandsEnderChest = islandsEnderChest;
221+
}
222+
public boolean isIslandsStatistics() {
223+
return islandsStatistics;
224+
}
225+
public void setIslandsStatistics(boolean islandsStatistics) {
226+
this.islandsStatistics = islandsStatistics;
227+
}
147228

148229
}

0 commit comments

Comments
 (0)