Skip to content

Conversation

@m1919810
Copy link

@m1919810 m1919810 commented Jan 7, 2026

简介

相关的 Issues (没有可不填)

重构部分背包代码 对应#1181

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the backpack system to address issues related to #1181. The refactoring focuses on improving concurrency handling, introducing a snapshot-based change tracking mechanism, and modernizing the async callback pattern using CompletableFuture.

  • Introduces ThreadUtils utility for safer main thread execution using NMS reflection with fallback
  • Refactors backpack inventory tracking using InvSnapshot for better change detection
  • Replaces callback-based async operations with CompletableFuture for cleaner async code

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
ThreadUtils.java New utility class for thread-safe main thread execution with NMS reflection fallback
BackpackListener.java Major refactor of backpack open/close logic with improved tracking and validation
PlayerBackpack.java Adds snapshot mechanism and CompletableFuture-based async API
InvSnapshot.java New immutable snapshot class for inventory state tracking
ProfileDataController.java Refactors saveBackpackInventory to compute changed slots internally with synchronization
AbstractCraftingTable.java Simplifies backpack upgrade logic using new CompletableFuture API
CoolerListener.java Updates saveBackpackInventory call to remove slot parameter
InvStorageUtils.java Adds overload to accept InvSnapshot directly
BackpackCache.java Changes visibility from package-private to public
BlockDataController.java Updates to use InvSnapshot class instead of raw List
PlayerProfileMigrator.java Updates saveBackpackInventory call in migration code
ADataController.java Adds @Getter annotations for executor access
IAsyncReadCallback.java Marks interface as deprecated

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 262 to 264
// } else {
// Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
// }
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented-out code should be removed. Lines 262-264 contain leftover code that serves no purpose and reduces code readability. If this is related to the "already-open" check logic, it should either be properly implemented or completely removed.

Suggested change
// } else {
// Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
// }

Copilot uses AI. Check for mistakes.
return;
}
// Check if the item is modified during the chat input
if (!Objects.equals(itemCopy, item)) {
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The item comparison on line 204 uses Objects.equals which checks for exact object equality. However, the comparison should check if the ItemStack has been modified in terms of its properties (type, amount, metadata), not object reference. Use ItemStack.equals() or ItemStack.isSimilar() instead for proper item comparison.

Suggested change
if (!Objects.equals(itemCopy, item)) {
if (!itemCopy.equals(item)) {

Copilot uses AI. Check for mistakes.
Comment on lines 68 to 73
if (invSnapshot.remove(backpack.getUniqueId()) == null) {
reportBackpackInvCacheError(p, backpack, null);
}
// The changedSlot computation and refreshSnapshot is moved to the
// ProfileDataController#saveBackpackInventory
Slimefun.getDatabaseManager().getProfileDataController().saveBackpackInventory(backpack);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check for InvSnapshot. The code removes the InvSnapshot from the map (line 68) and checks if it's null (line 68-70), but then on line 73 calls saveBackpackInventory without ensuring that the snapshot exists in the PlayerBackpack. If the snapshot was null and reported as an error, the save operation should probably be handled differently or skipped.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +75
* If the input is already a FutureTask, it is cast and returned.
* Otherwise, a new FutureTask is created with the Runnable and null result.
*
* @param runnable The runnable to wrap
* @return A FutureTask wrapping the runnable
*/
public static FutureTask<Void> getFutureTask(Runnable runnable) {
return runnable instanceof FutureTask<?> future
? (FutureTask<Void>) future
: new FutureTask<>(runnable, (Void) null);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential unsafe cast. On line 74, the code casts FutureTask<?> to FutureTask<Void>, but if the FutureTask was created with a different result type, this cast could lead to ClassCastException when the result is retrieved. Consider checking the result type or documenting that this method should only be called with Runnable-based FutureTasks that return Void.

Suggested change
* If the input is already a FutureTask, it is cast and returned.
* Otherwise, a new FutureTask is created with the Runnable and null result.
*
* @param runnable The runnable to wrap
* @return A FutureTask wrapping the runnable
*/
public static FutureTask<Void> getFutureTask(Runnable runnable) {
return runnable instanceof FutureTask<?> future
? (FutureTask<Void>) future
: new FutureTask<>(runnable, (Void) null);
* If the input is already a FutureTask, it is returned as-is.
* Otherwise, a new FutureTask is created with the Runnable and a null result.
*
* @param runnable The runnable to wrap
* @return A FutureTask wrapping the runnable
*/
public static FutureTask<?> getFutureTask(Runnable runnable) {
return runnable instanceof FutureTask<?> future
? future
: new FutureTask<>(runnable, null);

Copilot uses AI. Check for mistakes.
Comment on lines +257 to 264
/**
* This refreshes the internal snapshot,
* It should be called after every database writing task
* It should not be called elsewhere
*/
public void refreshSnapshot() {
this.snapshot = new InvSnapshot(inventory);
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation for the refreshSnapshot method. While there is a comment (lines 257-261), it lacks proper JavaDoc format and doesn't clearly explain the threading requirements, synchronization guarantees, or what happens if it's called at the wrong time. Consider converting to proper JavaDoc with @throws or @implNote tags as appropriate.

Copilot uses AI. Check for mistakes.
Comment on lines 345 to 386
@Deprecated(forRemoval = true)
public void saveBackpackInventory(PlayerBackpack bp, Set<Integer> slotsIgnored) {
// we decided to compute slots internal, and the argument is ignored to avoid potential data desync
saveBackpackInventory(bp);
}

public void saveBackpackInventory(@Nonnull PlayerBackpack bp) {
// avoid asynchronize save
synchronized (bp) {
Set<Integer> slots = bp.getSnapshot().getChangedSlots(bp.getInventory());
bp.refreshSnapshot();
var id = bp.getUniqueId().toString();
var inv = bp.getInventory();
slots.forEach(slot -> {
var key = new RecordKey(DataScope.BACKPACK_INVENTORY);
key.addCondition(FieldKey.BACKPACK_ID, id);
key.addCondition(FieldKey.INVENTORY_SLOT, slot + "");
key.addField(FieldKey.INVENTORY_ITEM);
var is = inv.getItem(slot);
if (is == null) {
scheduleDeleteTask(new UUIDKey(DataScope.NONE, bp.getOwner().getUniqueId()), key, false);
} else {
try {
var data = new RecordSet();
data.put(FieldKey.BACKPACK_ID, id);
data.put(FieldKey.INVENTORY_SLOT, slot + "");
data.put(FieldKey.INVENTORY_ITEM, is);
scheduleWriteTask(
new UUIDKey(DataScope.NONE, bp.getOwner().getUniqueId()), key, data, false);
} catch (IllegalArgumentException e) {
Slimefun.logger().log(Level.WARNING, e.getMessage());
}
}
}
});
});
}
}

@Deprecated(forRemoval = true)
public void saveBackpackInventory(PlayerBackpack bp, Integer... slots) {
saveBackpackInventory(bp, Set.of(slots));
// we decided to compute slots internal, and the argument is ignored to avoid potential data desync
saveBackpackInventory(bp);
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear deprecation reasoning. The deprecation annotations on lines 345 and 382 state "forRemoval = true" but the implementation comment says the argument is "ignored to avoid potential data desync." This is confusing - if the method is kept for backward compatibility but behaves differently than expected, this could break existing code silently. Consider either removing these methods entirely or documenting the behavioral change more clearly in the deprecation message.

Copilot uses AI. Check for mistakes.
Comment on lines 240 to 259
// Check if someone else is currently viewing this backpack
if (backpacks.containsValue(bp.getUniqueId())
|| !bp.getInventory().getViewers().isEmpty()) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
return;
}
// If backpacks doesn't contain the bp uniqueId, then the existence of bp's invSnapshot must
// be a bug
if (invSnapshot.containsKey(bp.getUniqueId())) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
reportBackpackInvCacheError(p, bp, invSnapshot.remove(bp.getUniqueId()));
return;
}
SoundEffect.BACKPACK_OPEN_SOUND.playAt(p.getLocation(), SoundCategory.PLAYERS);
bp.open(p);
backpacks.put(p.getUniqueId(), bp.getUniqueId());
backpackInstances.put(p.getUniqueId(), backpackItem);
// create a InvSnapshot just for fun (bs
// should not call bp.refreshSnapshot()
invSnapshot.put(bp.getUniqueId(), new InvSnapshot(bp.getInventory()));
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition in backpack opening logic. Between the check on line 241-242 (checking if backpack is already open) and line 255 (adding to backpacks map), another thread could open the same backpack, resulting in multiple viewers. Consider moving the check and map insertion into a synchronized block or using a ConcurrentHashMap with putIfAbsent to ensure atomicity.

Suggested change
// Check if someone else is currently viewing this backpack
if (backpacks.containsValue(bp.getUniqueId())
|| !bp.getInventory().getViewers().isEmpty()) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
return;
}
// If backpacks doesn't contain the bp uniqueId, then the existence of bp's invSnapshot must
// be a bug
if (invSnapshot.containsKey(bp.getUniqueId())) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
reportBackpackInvCacheError(p, bp, invSnapshot.remove(bp.getUniqueId()));
return;
}
SoundEffect.BACKPACK_OPEN_SOUND.playAt(p.getLocation(), SoundCategory.PLAYERS);
bp.open(p);
backpacks.put(p.getUniqueId(), bp.getUniqueId());
backpackInstances.put(p.getUniqueId(), backpackItem);
// create a InvSnapshot just for fun (bs
// should not call bp.refreshSnapshot()
invSnapshot.put(bp.getUniqueId(), new InvSnapshot(bp.getInventory()));
synchronized (backpacks) {
// Check if someone else is currently viewing this backpack
if (backpacks.containsValue(bp.getUniqueId())
|| !bp.getInventory().getViewers().isEmpty()) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
return;
}
// If backpacks doesn't contain the bp uniqueId, then the existence of bp's invSnapshot must
// be a bug
if (invSnapshot.containsKey(bp.getUniqueId())) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
reportBackpackInvCacheError(p, bp, invSnapshot.remove(bp.getUniqueId()));
return;
}
SoundEffect.BACKPACK_OPEN_SOUND.playAt(p.getLocation(), SoundCategory.PLAYERS);
bp.open(p);
backpacks.put(p.getUniqueId(), bp.getUniqueId());
backpackInstances.put(p.getUniqueId(), backpackItem);
// create a InvSnapshot just for fun (bs
// should not call bp.refreshSnapshot()
invSnapshot.put(bp.getUniqueId(), new InvSnapshot(bp.getInventory()));
}

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +58
@Nonnull
@Getter
private InvSnapshot snapshot;
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot field is marked as @nonnull (line 56) but is not initialized in the field declaration. While it is initialized in the constructor (line 254), there could be a brief moment where the field is null if accessed before initialization completes, especially in a concurrent environment. Consider initializing the field inline or using a factory method pattern to ensure the object is fully constructed before being shared.

Copilot uses AI. Check for mistakes.
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;

@Getter
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation for public class. The InvSnapshot class is a new public class but lacks class-level JavaDoc documentation. Add documentation explaining what an InvSnapshot is, when it should be used, its immutability guarantees, and typical usage patterns.

Suggested change
@Getter
@Getter
/**
* Immutable snapshot of an inventory's contents.
* <p>
* An {@code InvSnapshot} captures the state of an {@link Inventory} (or a raw
* {@link ItemStack} array) at a specific point in time. The underlying list of
* item/slot pairs is defensively copied on construction and cannot be modified
* afterwards, ensuring that the snapshot remains stable even if the original
* inventory or array is later changed.
* </p>
* <p>
* This class is typically used to detect which slots have changed between two
* inventory states. A common pattern is:
* </p>
* <pre>{@code
* InvSnapshot before = new InvSnapshot(inventory);
* // ... mutate the inventory ...
* Set<Integer> changedSlots = before.getChangedSlots(inventory);
* }</pre>
* <p>
* Use {@link #getChangedSlots(ItemStack[])} or {@link #getChangedSlots(Inventory)}
* to compare the stored snapshot against a current inventory view and obtain the
* set of slot indices whose contents differ.
* </p>
*/

Copilot uses AI. Check for mistakes.

if (CommonPatterns.NUMERIC.matcher(splitLine[1]).matches()) {
uuid = splitLine[0].replace(ChatColors.color("&7ID: "), "");
id = OptionalInt.of(Integer.parseInt(splitLine[1]));
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential uncaught 'java.lang.NumberFormatException'.

Suggested change
id = OptionalInt.of(Integer.parseInt(splitLine[1]));
try {
id = OptionalInt.of(Integer.parseInt(splitLine[1]));
} catch (NumberFormatException ignored) {
// Ignore invalid numeric values and leave 'id' empty
}

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
if (item.getAmount() > 1) {
Slimefun.getLocalization().sendMessage(p, "backpack.no-stack", true);
// Check if the player moves the item
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains a grammatical error. It should be "Check if the player moves the item" or "Check if the player moved the item".

Copilot uses AI. Check for mistakes.
import java.util.function.Consumer;

/**
* This interface is deprecated and related methods will be replaced by methods using {@link java.util.concurrent.CompletableFuture} in the future, <- this work is a completable future
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation contains a grammatical error. "this work is a completable future" should be "this work aims to replace them with CompletableFuture" or similar.

Suggested change
* This interface is deprecated and related methods will be replaced by methods using {@link java.util.concurrent.CompletableFuture} in the future, <- this work is a completable future
* This interface is deprecated and related methods will be replaced by methods using {@link java.util.concurrent.CompletableFuture} in the future; this work aims to replace them with {@link java.util.concurrent.CompletableFuture}.

Copilot uses AI. Check for mistakes.
Comment on lines 53 to 55
private final Map<UUID, UUID> backpacks = new HashMap<>();
private final Map<UUID, SlimefunBackpack> backpackInstances = new HashMap<>();
private final Map<UUID, InvSnapshot> invSnapshot = new HashMap<>();
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The maps (backpacks, backpackInstances, invSnapshot) are not thread-safe. Since they can be accessed from multiple threads (main thread for events and async threads for database operations), using HashMap instead of ConcurrentHashMap can lead to race conditions and potential data corruption. Consider using ConcurrentHashMap for thread-safety.

Copilot uses AI. Check for mistakes.
}
// Check if the item is modified during the chat input
if (!Objects.equals(itemCopy, item)) {
Slimefun.getLocalization().sendMessage(p, "backpack.not-original-item", true);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is being sent to the original player 'p' instead of the resolved 'player' variable. Since the player reference has been resolved and null-checked at line 195-196, messages should be sent to 'player' to ensure consistency.

Copilot uses AI. Check for mistakes.
Player player = Bukkit.getPlayer(puuid);
if (player == null) return;
var pInv = player.getInventory();
// Check if the player change the amount of item
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains a grammatical error. It should be "Check if the player changes the amount of item" or "Check if the player changed the amount of item".

Suggested change
// Check if the player change the amount of item
// Check if the player changes the amount of the item

Copilot uses AI. Check for mistakes.
if (backpack == null) {
return Optional.empty();
}
onReadyCb.run();
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback is executed unconditionally even when result is null. This means if the backpack retrieval fails, the callback still runs, potentially causing issues. Consider only running the callback when the operation succeeds, or handle the null case appropriately in the callback.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +145
// var changedSlot = new HashSet<Integer>();

for (String key : configFile.getKeys("backpacks." + bpID + ".contents")) {
var bpKey = Integer.parseInt(key);
var item = configFile.getItem("backpacks." + bpID + ".contents." + bpKey);
bp.getInventory().setItem(bpKey, item);
changedSlot.add(bpKey);
// changedSlot.add(bpKey);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These commented-out lines should be removed rather than left in the codebase. If they're not needed anymore, they should be deleted. The commit history serves as a reference for previous implementations.

Copilot uses AI. Check for mistakes.
var pInv = player.getInventory();
// Check if the player change the amount of item
if (item.getAmount() != 1) {
Slimefun.getLocalization().sendMessage(p, "backpack.no-stack", true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Slimefun.getLocalization().sendMessage(p, "backpack.no-stack", true);
Slimefun.getLocalization().sendMessage(player, "backpack.no-stack", true);

}
// Check if the item is modified during the chat input
if (!Objects.equals(itemCopy, item)) {
Slimefun.getLocalization().sendMessage(p, "backpack.not-original-item", true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Slimefun.getLocalization().sendMessage(p, "backpack.not-original-item", true);
Slimefun.getLocalization().sendMessage(player, "backpack.not-original-item", true);

Slimefun.getLocalization().sendMessage(p, "backpack.no-stack", true);
// Check if the player moves the item
if (!item.equals(pInv.getItemInMainHand()) && !item.equals(pInv.getItemInOffHand())) {
Slimefun.getLocalization().sendMessage(p, "backpack.not-original-item", true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Slimefun.getLocalization().sendMessage(p, "backpack.not-original-item", true);
Slimefun.getLocalization().sendMessage(player, "backpack.not-original-item", true);

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +247 to +254
if (contents != null) {
if (size != contents.length) {
throw new IllegalArgumentException("Invalid contents: size mismatched!");
}
inventory.setContents(contents);
}

if (size != contents.length) {
throw new IllegalArgumentException("Invalid contents: size mismatched!");
}
inventory.setContents(contents);
this.snapshot = new InvSnapshot(inventory);
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When contents is null, the snapshot is created from an empty inventory. However, when contents is not null but the size doesn't match, an exception is thrown before the snapshot can be initialized. This leaves the @nonnull snapshot field uninitialized, which can cause NullPointerException later. The exception should be thrown before the super constructor call, or the snapshot should be initialized in a finally block or at the field level.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +15
* This interface is deprecated and related methods will be replaced by methods using {@link java.util.concurrent.CompletableFuture} in the future, <- this work is a completable future
* related methods will be kept and mark deprecated
* you can run your original {@link IAsyncReadCallback} callbacks using {@link java.util.concurrent.CompletableFuture#thenAcceptAsync(Consumer, java.util.concurrent.Executor)}, don't forget to check whether the accepted value is null or not
* If you want to invoke callback not on the main thread, use
* {@link ADataController#getCallbackExecutor}
* If you want to invoke callback on the main thread , see
* {@link ThreadUtils#getMainThreadExecutor()} or {@link ThreadUtils#getMainDelayedExecutor()}
* @param <T>
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment contains a typo and grammatical error. The phrase "related methods will be replaced by methods using CompletableFuture in the future, <- this work is a completable future" is unclear. Consider revising to: "This interface is deprecated. Related methods are being replaced by methods using CompletableFuture."

Suggested change
* This interface is deprecated and related methods will be replaced by methods using {@link java.util.concurrent.CompletableFuture} in the future, <- this work is a completable future
* related methods will be kept and mark deprecated
* you can run your original {@link IAsyncReadCallback} callbacks using {@link java.util.concurrent.CompletableFuture#thenAcceptAsync(Consumer, java.util.concurrent.Executor)}, don't forget to check whether the accepted value is null or not
* If you want to invoke callback not on the main thread, use
* {@link ADataController#getCallbackExecutor}
* If you want to invoke callback on the main thread , see
* {@link ThreadUtils#getMainThreadExecutor()} or {@link ThreadUtils#getMainDelayedExecutor()}
* @param <T>
* This interface is deprecated. Related methods are being replaced by methods using {@link java.util.concurrent.CompletableFuture}.
* Existing related methods will be kept and marked as deprecated for backward compatibility.
* You can run your original {@link IAsyncReadCallback} callbacks using
* {@link java.util.concurrent.CompletableFuture#thenAcceptAsync(Consumer, java.util.concurrent.Executor)}; don't forget to check whether the accepted value is null or not.
* If you want to invoke the callback not on the main thread, use
* {@link ADataController#getCallbackExecutor}.
* If you want to invoke the callback on the main thread, see
* {@link ThreadUtils#getMainThreadExecutor()} or {@link ThreadUtils#getMainDelayedExecutor()}.
* @param <T> the type of the result

Copilot uses AI. Check for mistakes.
Comment on lines +345 to +348
@Deprecated(forRemoval = true)
public void saveBackpackInventory(PlayerBackpack bp, Set<Integer> slotsIgnored) {
// we decided to compute slots internal, and the argument is ignored to avoid potential data desync
saveBackpackInventory(bp);
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name "slotsIgnored" accurately describes that the parameter is ignored, which is good. However, the method implementation now computes slots internally, which means calling code that relies on specifying exact slots will have different behavior. This is a breaking change in behavior, not just a deprecation. Consider documenting this behavioral change more prominently in the deprecation notice or providing a migration guide.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +17
public InvSnapshot(List<Pair<ItemStack, Integer>> snapshot) {
this.snapshot = List.copyOf(snapshot);
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot is immutable by using List.copyOf(), which is good for thread safety. However, the ItemStack objects within the list are still mutable. If ItemStacks are modified elsewhere, the snapshot integrity could be compromised. Consider creating defensive copies of ItemStacks within the snapshot.

Copilot uses AI. Check for mistakes.
Comment on lines +264 to +272
if (backpacks.containsValue(bp.getUniqueId())
|| !bp.getInventory().getViewers().isEmpty()) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
return;
}
SoundEffect.BACKPACK_OPEN_SOUND.playAt(p.getLocation(), SoundCategory.PLAYERS);
bp.open(p);
backpacks.put(p.getUniqueId(), bp.getUniqueId());
backpackInstances.put(p.getUniqueId(), backpackItem);
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a race condition between checking if the backpack is already open (line 264) and actually opening it (line 270-271). Another thread could open the same backpack between these operations. The check uses containsValue() which searches through all values, and then later uses put() which could overwrite. Consider using a more atomic operation or additional synchronization to prevent multiple viewers from opening the same backpack simultaneously.

Suggested change
if (backpacks.containsValue(bp.getUniqueId())
|| !bp.getInventory().getViewers().isEmpty()) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
return;
}
SoundEffect.BACKPACK_OPEN_SOUND.playAt(p.getLocation(), SoundCategory.PLAYERS);
bp.open(p);
backpacks.put(p.getUniqueId(), bp.getUniqueId());
backpackInstances.put(p.getUniqueId(), backpackItem);
synchronized (backpacks) {
if (backpacks.containsValue(bp.getUniqueId())
|| !bp.getInventory().getViewers().isEmpty()) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
return;
}
SoundEffect.BACKPACK_OPEN_SOUND.playAt(p.getLocation(), SoundCategory.PLAYERS);
bp.open(p);
backpacks.put(p.getUniqueId(), bp.getUniqueId());
backpackInstances.put(p.getUniqueId(), backpackItem);
}

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +80
if (!openingPlayers.isEmpty() && openingPlayers.contains(e.getPlayer().getUniqueId())) {
e.setCancelled(true);
return;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The openingPlayers set is checked using isEmpty() before contains(). This is an unnecessary optimization that adds complexity. The contains() operation on a HashSet is O(1) regardless of whether isEmpty() is checked first. Consider removing the isEmpty() checks throughout this file to simplify the code.

Copilot uses AI. Check for mistakes.
Comment on lines +226 to 228
if (!Objects.equals(itemCopy, item)) {
Slimefun.getLocalization().sendMessage(player, "backpack.not-original-item", true);
return;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The item is compared using equals() with the itemCopy that was cloned earlier. However, ItemStack.equals() compares object identity by default in some implementations, not deep equality. Consider using a more reliable comparison method such as ItemStack.isSimilar() or comparing the serialized data to detect if the item has been modified.

Copilot uses AI. Check for mistakes.
Comment on lines +254 to +274
openingPlayers.add(p.getUniqueId());
PlayerBackpack.getAsync(item)
.thenAcceptAsync(
bp -> {
openingPlayers.remove(p.getUniqueId());
// Check if the backpack item contains invalid backpack data
if (bp == null || bp.isInvalid()) {
return;
}
// Check if someone else is currently viewing this backpack
if (backpacks.containsValue(bp.getUniqueId())
|| !bp.getInventory().getViewers().isEmpty()) {
Slimefun.getLocalization().sendMessage(p, "backpack.already-open", true);
return;
}
SoundEffect.BACKPACK_OPEN_SOUND.playAt(p.getLocation(), SoundCategory.PLAYERS);
bp.open(p);
backpacks.put(p.getUniqueId(), bp.getUniqueId());
backpackInstances.put(p.getUniqueId(), backpackItem);
},
ThreadUtils.getMainThreadExecutor());
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The player is removed from openingPlayers inside the async callback, but if an exception occurs during backpack loading or the callback is never executed, the player will remain in the openingPlayers set indefinitely. This will block the player from interacting with items, entities, or opening other backpacks. Consider using a try-finally block or adding timeout handling to ensure cleanup.

Copilot uses AI. Check for mistakes.
@StarWishsama StarWishsama merged commit 8f6affb into SlimefunGuguProject:dev Jan 8, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants