From 9378a85538a2803321c5ade1871dbd1d560c908f Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Tue, 30 Dec 2025 22:19:17 -0600 Subject: [PATCH 01/12] Verify backup removal Changed the removeBackup method to attempt to delete the backup file up to 5 times. If backup removal fails during inventory restore, the player is disconnected to prevent an old inventory backup from being restored in the future. --- .../cn/alini/offlineauth/OfflineAuthHandler.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index c747775..3383532 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -276,10 +276,15 @@ private static ItemStack[] loadBackupInventory(String name, int size) { return null; } } - private static void removeBackup(String name) { + private static boolean removeBackup(String name) { inventoryBackup.remove(name); File file = new File(INVENTORY_DIR, name + ".json"); - if (file.exists()) file.delete(); + if (!file.exists()) return true; + for (int i = 0; i < 5; i++) { + if (file.delete()) return true; + try { Thread.sleep(20); } catch (InterruptedException e) {} + } + return !file.exists(); } private static void restoreInventoryIfNeeded(ServerPlayer player) { String name = player.getName().getString(); @@ -288,7 +293,10 @@ private static void restoreInventoryIfNeeded(ServerPlayer player) { for (int i = 0; i < backup.length; i++) { player.getInventory().setItem(i, backup[i]); } - removeBackup(name); + if (!removeBackup(name)) { + player.connection.disconnect(Component.literal("Critical Error: Failed to clear inventory backup. Login aborted to prevent data loss.")); + return; + } player.sendSystemMessage(Component.literal(config.msg("inventory_restored"))); } } From 9630c0e98729addffa26ea88f308c4ad5ad38ff1 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Tue, 30 Dec 2025 22:58:53 -0600 Subject: [PATCH 02/12] Inventory backup file extension to .dat Updated all references in OfflineAuthHandler to use '.dat' instead of '.json' for inventory backup files as these binary not json. --- .../java/cn/alini/offlineauth/OfflineAuthHandler.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index 3383532..fdf39b3 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -240,7 +240,7 @@ private static void backupInventory(String name, ItemStack[] inv) { inventoryBackup.put(name, inv); File dir = new File(INVENTORY_DIR); if (!dir.exists()) dir.mkdirs(); - File file = new File(dir, name + ".json"); + File file = new File(dir, name + ".dat"); try (FileOutputStream fos = new FileOutputStream(file)) { ListTag list = new ListTag(); for (ItemStack stack : inv) { @@ -254,12 +254,12 @@ private static void backupInventory(String name, ItemStack[] inv) { } } private static boolean hasInventoryFile(String name) { - File file = new File(INVENTORY_DIR, name + ".json"); + File file = new File(INVENTORY_DIR, name + ".dat"); return file.exists(); } private static ItemStack[] loadBackupInventory(String name, int size) { if (inventoryBackup.containsKey(name)) return inventoryBackup.get(name); - File file = new File(INVENTORY_DIR, name + ".json"); + File file = new File(INVENTORY_DIR, name + ".dat"); if (!file.exists()) return null; try (FileInputStream fis = new FileInputStream(file)) { CompoundTag root = NbtIo.readCompressed(fis); @@ -278,7 +278,7 @@ private static ItemStack[] loadBackupInventory(String name, int size) { } private static boolean removeBackup(String name) { inventoryBackup.remove(name); - File file = new File(INVENTORY_DIR, name + ".json"); + File file = new File(INVENTORY_DIR, name + ".dat"); if (!file.exists()) return true; for (int i = 0; i < 5; i++) { if (file.delete()) return true; From 2a5b8270818dca050feab97c4b066f22c16c8344 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Tue, 30 Dec 2025 23:13:11 -0600 Subject: [PATCH 03/12] Fixed overwriting existing config Fixed overwriting existing config file on every start of the mod. Now changes to the config will actually persist. --- src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java | 2 +- src/main/java/cn/alini/offlineauth/Offlineauth.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index c747775..4d962a0 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -44,7 +44,7 @@ public class OfflineAuthHandler { private static final Gson gson = new Gson(); private static final Map autoLoginMap = new HashMap<>(); private static final Map failMap = new HashMap<>(); - static { loadAutoLogin(); loadFail(); } + static { config.load(); loadAutoLogin(); loadFail(); } private static boolean isOfflinePlayer(ServerPlayer player) { return !TrueuuidApi.isPremium(player.getName().getString().toLowerCase(Locale.ROOT)); diff --git a/src/main/java/cn/alini/offlineauth/Offlineauth.java b/src/main/java/cn/alini/offlineauth/Offlineauth.java index 38d47b5..9f8dc83 100644 --- a/src/main/java/cn/alini/offlineauth/Offlineauth.java +++ b/src/main/java/cn/alini/offlineauth/Offlineauth.java @@ -13,6 +13,5 @@ public class Offlineauth { public Offlineauth() { //mod成功加载日志 LOGGER.info("OfflineAuth mod loaded successfully!"); - new AuthConfig().save(); } } From 0ec9a6098a09b2542c96e13f31b12eb7006a31ed Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Tue, 30 Dec 2025 23:15:41 -0600 Subject: [PATCH 04/12] Update trueuuid version and mod version trueuuid library from 1.0.2 to 1.0.5 in build.gradle and changed mod_version to 1.0.3 in gradle.properties. --- build.gradle | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7997ff1..0185c69 100644 --- a/build.gradle +++ b/build.gradle @@ -149,7 +149,7 @@ dependencies { // If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"], // then special handling is done to allow a setup of a vanilla dependency without the use of an external repository. minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" - implementation files('libs/trueuuid-1.0.2.jar') + implementation files('libs/trueuuid-1.0.5.jar') // Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}") diff --git a/gradle.properties b/gradle.properties index 261059b..41c7b08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,7 +38,7 @@ mod_name=OfflineAuth # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=GNU LGPL 3.0 # The mod version. See https://semver.org/ -mod_version=1.0.1 +mod_version=1.0.3 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html From f185cecc9f70196848bbfb33f3f22b45829dad9e Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 00:26:27 -0600 Subject: [PATCH 05/12] Refactor inventory backup to use full NBT backup Changed inventory backup and restore to use full CompoundTag NBT instead of just ItemStack. This adds compatibility with mods that store extra data in player NBT, such as Curios. Also added container closing on login tick. --- gradle.properties | 2 +- .../alini/offlineauth/OfflineAuthHandler.java | 50 ++++++++----------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/gradle.properties b/gradle.properties index 41c7b08..2fe92db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,7 +38,7 @@ mod_name=OfflineAuth # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=GNU LGPL 3.0 # The mod version. See https://semver.org/ -mod_version=1.0.3 +mod_version=1.0.4 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index 1cd13c8..4e4fba1 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -33,7 +33,7 @@ public class OfflineAuthHandler { private static final JsonAuthStorage storage = new JsonAuthStorage(); private static final AuthConfig config = new AuthConfig(); private static final Set loggedIn = new HashSet<>(); - private static final Map inventoryBackup = new HashMap<>(); + private static final Map inventoryBackup = new HashMap<>(); private static final Map joinTimeMap = new HashMap<>(); private static final Map notLoggedTick = new HashMap<>(); private static final Map notLoggedSpawnPos = new HashMap<>(); @@ -129,11 +129,7 @@ public static void onPlayerLogin(PlayerLoggedInEvent event) { // 背包暂存,防止未登录时操作物品 if (!inventoryBackup.containsKey(name) && !hasInventoryFile(name)) { - ItemStack[] inv = new ItemStack[player.getInventory().getContainerSize()]; - for (int i = 0; i < inv.length; i++) { - inv[i] = player.getInventory().getItem(i).copy(); - } - backupInventory(name, inv); + backupInventory(name, player); player.getInventory().clearContent(); } else if (!inventoryBackup.containsKey(name) && hasInventoryFile(name)) { player.getInventory().clearContent(); @@ -161,6 +157,9 @@ public static void onPlayerTick(TickEvent.PlayerTickEvent event) { return; } int tick = notLoggedTick.getOrDefault(name, 0) + 1; + if (player.containerMenu != player.inventoryMenu) { + player.closeContainer(); + } if (tick >= 100) { // 5秒=100tick if (!storage.isRegistered(name)) { player.sendSystemMessage(Component.literal(config.msg("register_prompt"))); @@ -236,41 +235,30 @@ public static void onRightClick(PlayerInteractEvent.RightClickItem event) { } } - private static void backupInventory(String name, ItemStack[] inv) { - inventoryBackup.put(name, inv); + private static void backupInventory(String name, ServerPlayer player) { + CompoundTag tag = new CompoundTag(); + player.saveWithoutId(tag); + inventoryBackup.put(name, tag); File dir = new File(INVENTORY_DIR); if (!dir.exists()) dir.mkdirs(); File file = new File(dir, name + ".dat"); try (FileOutputStream fos = new FileOutputStream(file)) { - ListTag list = new ListTag(); - for (ItemStack stack : inv) { - list.add(stack.save(new CompoundTag())); - } - CompoundTag root = new CompoundTag(); - root.put("items", list); - NbtIo.writeCompressed(root, fos); + NbtIo.writeCompressed(tag, fos); } catch (Exception e) { e.printStackTrace(); } } private static boolean hasInventoryFile(String name) { - File file = new File(INVENTORY_DIR, name + ".dat"); - return file.exists(); + return new File(INVENTORY_DIR, name + ".dat").exists(); } - private static ItemStack[] loadBackupInventory(String name, int size) { + private static CompoundTag loadBackupInventory(String name) { if (inventoryBackup.containsKey(name)) return inventoryBackup.get(name); File file = new File(INVENTORY_DIR, name + ".dat"); if (!file.exists()) return null; try (FileInputStream fis = new FileInputStream(file)) { CompoundTag root = NbtIo.readCompressed(fis); - ListTag list = root.getList("items", 10); - ItemStack[] inv = new ItemStack[size]; - for (int i = 0; i < inv.length && i < list.size(); i++) { - inv[i] = ItemStack.of(list.getCompound(i)); - } - for (int i = list.size(); i < inv.length; i++) inv[i] = ItemStack.EMPTY; - inventoryBackup.put(name, inv); - return inv; + inventoryBackup.put(name, root); + return root; } catch (Exception e) { e.printStackTrace(); return null; @@ -288,15 +276,17 @@ private static boolean removeBackup(String name) { } private static void restoreInventoryIfNeeded(ServerPlayer player) { String name = player.getName().getString(); - ItemStack[] backup = loadBackupInventory(name, player.getInventory().getContainerSize()); + CompoundTag backup = loadBackupInventory(name); if (backup != null) { - for (int i = 0; i < backup.length; i++) { - player.getInventory().setItem(i, backup[i]); - } + // Full NBT restore (supports mod data like Curios, etc) + player.load(backup); + if (!removeBackup(name)) { player.connection.disconnect(Component.literal("Critical Error: Failed to clear inventory backup. Login aborted to prevent data loss.")); return; } + player.inventoryMenu.broadcastChanges(); + player.containerMenu.broadcastChanges(); player.sendSystemMessage(Component.literal(config.msg("inventory_restored"))); } } From 5619d31550a67a826b13c73307bf4e402a8d0073 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 00:27:38 -0600 Subject: [PATCH 06/12] Improved kick message --- src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index 4e4fba1..1c256c3 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -282,7 +282,7 @@ private static void restoreInventoryIfNeeded(ServerPlayer player) { player.load(backup); if (!removeBackup(name)) { - player.connection.disconnect(Component.literal("Critical Error: Failed to clear inventory backup. Login aborted to prevent data loss.")); + player.connection.disconnect(Component.literal("offlineauth Server Error: Failed to clear inventory backup. Login aborted to prevent data loss. Contact server owner for assistance.")); return; } player.inventoryMenu.broadcastChanges(); From 6ee44f1161e7133bbf3d7902b4a8c7c647a8bf80 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 00:58:54 -0600 Subject: [PATCH 07/12] Console log for failed backup deletion Added SLF4J logger and an error log when the server fails to delete a player's inventory backup during login alongside kicking the user. --- src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index 1c256c3..4b3d041 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -23,6 +23,8 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import cn.alini.trueuuid.api.TrueuuidApi; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; import java.io.*; import java.lang.reflect.Type; @@ -30,6 +32,7 @@ @Mod.EventBusSubscriber public class OfflineAuthHandler { + private static final Logger LOGGER = LogUtils.getLogger(); private static final JsonAuthStorage storage = new JsonAuthStorage(); private static final AuthConfig config = new AuthConfig(); private static final Set loggedIn = new HashSet<>(); @@ -282,6 +285,7 @@ private static void restoreInventoryIfNeeded(ServerPlayer player) { player.load(backup); if (!removeBackup(name)) { + LOGGER.error("CRITICAL: Failed to delete inventory backup for player {}! Kicking player to prevent data loss.", name); player.connection.disconnect(Component.literal("offlineauth Server Error: Failed to clear inventory backup. Login aborted to prevent data loss. Contact server owner for assistance.")); return; } From c884a2e5a3d12024f62bb3d3739da01477b2d2de Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 01:00:51 -0600 Subject: [PATCH 08/12] Config reload command Added '/reload' command for reloading configuration while running, available to users with permission level 2, and added success message. --- gradle.properties | 2 +- src/main/java/cn/alini/offlineauth/AuthConfig.java | 1 + .../java/cn/alini/offlineauth/OfflineAuthHandler.java | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 41c7b08..2fe92db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,7 +38,7 @@ mod_name=OfflineAuth # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=GNU LGPL 3.0 # The mod version. See https://semver.org/ -mod_version=1.0.3 +mod_version=1.0.4 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/cn/alini/offlineauth/AuthConfig.java b/src/main/java/cn/alini/offlineauth/AuthConfig.java index 31a92ad..e85fee2 100644 --- a/src/main/java/cn/alini/offlineauth/AuthConfig.java +++ b/src/main/java/cn/alini/offlineauth/AuthConfig.java @@ -55,6 +55,7 @@ public AuthConfig() { messages.put("help_login", "§e/login 密码 §7- 登录账户"); messages.put("help_changepwd", "§e/changepassword 旧密码 新密码 §7- 修改密码"); messages.put("auto_login_warn", "§e⚠已启用自动登录(同IP设备短时间内无需重复登录)。如在网吧/公共电脑请勿使用此功能,避免账号被盗。"); + messages.put("reload_success", "§a配置已重载!"); // 正确做法:在构造后,手动调用 load(),不要在构造里调用 // 由主类 new AuthConfig 后,再调用 config.load() diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index 4d962a0..ba500e2 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -546,6 +546,14 @@ public static void registerCommands(RegisterCommandsEvent event) { return 1; }) ) + .then(Commands.literal("reload") + .requires(source -> source.hasPermission(2)) + .executes(ctx -> { + config.load(); + ctx.getSource().sendSuccess(() -> Component.literal(config.msg("reload_success")), true); + return 1; + }) + ) ); } } \ No newline at end of file From 87c358656c19a48089ef88a8caf512403b1d0c87 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 15:36:21 -0600 Subject: [PATCH 09/12] Add reload command to README --- README.md | 1 + README_zh-CN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 184112e..f83b655 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Supports registration, login, inventory protection, brute-force prevention, and - `/login ` — Login to your account. - `/changepassword ` — Change your password. - `/auth help` — Show help information. +- `/auth reload` — Reload the configuration file (requires permission level 2). ## Configuration diff --git a/README_zh-CN.md b/README_zh-CN.md index df58081..28e1b82 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -22,6 +22,7 @@ - `/login <密码>` — 登录账户 - `/changepassword <旧密码> <新密码>` — 修改密码 - `/auth help` — 查看帮助信息 +- `/auth reload` — 重载配置文件(需要权限等级 2) ## 配置说明 From 53542d5181af90ea6ce59bf5839dd59f8f795f11 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 16:16:15 -0600 Subject: [PATCH 10/12] Preserve new items on inventory restore Captures items received while unauthenticated and merges them back into the player's inventory after a full NBT restore. Drops items that wont fit if the inventory is full. This should alleviate potential issues with mods or plugins that give users items on login or the like. --- .../cn/alini/offlineauth/OfflineAuthHandler.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index 4b3d041..93f680d 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -281,8 +281,24 @@ private static void restoreInventoryIfNeeded(ServerPlayer player) { String name = player.getName().getString(); CompoundTag backup = loadBackupInventory(name); if (backup != null) { + // Capture items received while unauthenticated (e.g. starter kits or mod items) + List newItems = new ArrayList<>(); + for (int i = 0; i < player.getInventory().getContainerSize(); i++) { + ItemStack stack = player.getInventory().getItem(i); + if (!stack.isEmpty()) { + newItems.add(stack.copy()); + } + } + // Full NBT restore (supports mod data like Curios, etc) player.load(backup); + + // Merge new items back in, dropping if inventory is full + for (ItemStack stack : newItems) { + if (!player.getInventory().add(stack)) { + player.drop(stack, false); + } + } if (!removeBackup(name)) { LOGGER.error("CRITICAL: Failed to delete inventory backup for player {}! Kicking player to prevent data loss.", name); From 1e65a79c740b0dfc92b0487b40123a184d4f1b21 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Wed, 31 Dec 2025 17:02:37 -0600 Subject: [PATCH 11/12] Inventory-only backup option Added an 'inventoryOnly' config option to allow backing up only player inventory instead of full NBT data. Updated backup, restore, and file logic to for both default full NBT, and inventory only backups, using different file extensions (.dat for full, .inv for inventory only). --- .../java/cn/alini/offlineauth/AuthConfig.java | 2 + .../alini/offlineauth/OfflineAuthHandler.java | 42 ++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/main/java/cn/alini/offlineauth/AuthConfig.java b/src/main/java/cn/alini/offlineauth/AuthConfig.java index e85fee2..9281332 100644 --- a/src/main/java/cn/alini/offlineauth/AuthConfig.java +++ b/src/main/java/cn/alini/offlineauth/AuthConfig.java @@ -20,6 +20,7 @@ public class AuthConfig { public int failLockSeconds = 60; public boolean autoLoginEnable = true; public boolean failBlockEnable = true; + public boolean inventoryOnly = false; // true = Only backup inventory | false = Backup full player NBT public String prefix = "§7[§bAuth§7] "; public Map messages = new HashMap<>(); @@ -78,6 +79,7 @@ public void load() { this.maxFailAttempts = loaded.maxFailAttempts; this.failLockSeconds = loaded.failLockSeconds; this.autoLoginEnable = loaded.autoLoginEnable; + this.inventoryOnly = loaded.inventoryOnly; this.failBlockEnable = loaded.failBlockEnable; this.prefix = loaded.prefix; if (loaded.messages != null) this.messages.putAll(loaded.messages); diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index d2256fb..fc385cf 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -240,24 +240,38 @@ public static void onRightClick(PlayerInteractEvent.RightClickItem event) { private static void backupInventory(String name, ServerPlayer player) { CompoundTag tag = new CompoundTag(); - player.saveWithoutId(tag); + String ext; + if (config.inventoryOnly) { + tag.put("Inventory", player.getInventory().save(new ListTag())); + ext = ".inv"; + } else { + player.saveWithoutId(tag); + ext = ".dat"; + } inventoryBackup.put(name, tag); File dir = new File(INVENTORY_DIR); if (!dir.exists()) dir.mkdirs(); - File file = new File(dir, name + ".dat"); + File file = new File(dir, name + ext); try (FileOutputStream fos = new FileOutputStream(file)) { NbtIo.writeCompressed(tag, fos); } catch (Exception e) { e.printStackTrace(); } } + private static File getBackupFile(String name) { + File dat = new File(INVENTORY_DIR, name + ".dat"); + if (dat.exists()) return dat; + File inv = new File(INVENTORY_DIR, name + ".inv"); + if (inv.exists()) return inv; + return null; + } private static boolean hasInventoryFile(String name) { - return new File(INVENTORY_DIR, name + ".dat").exists(); + return getBackupFile(name) != null; } private static CompoundTag loadBackupInventory(String name) { if (inventoryBackup.containsKey(name)) return inventoryBackup.get(name); - File file = new File(INVENTORY_DIR, name + ".dat"); - if (!file.exists()) return null; + File file = getBackupFile(name); + if (file == null) return null; try (FileInputStream fis = new FileInputStream(file)) { CompoundTag root = NbtIo.readCompressed(fis); inventoryBackup.put(name, root); @@ -269,8 +283,8 @@ private static CompoundTag loadBackupInventory(String name) { } private static boolean removeBackup(String name) { inventoryBackup.remove(name); - File file = new File(INVENTORY_DIR, name + ".dat"); - if (!file.exists()) return true; + File file = getBackupFile(name); + if (file == null) return true; for (int i = 0; i < 5; i++) { if (file.delete()) return true; try { Thread.sleep(20); } catch (InterruptedException e) {} @@ -279,8 +293,9 @@ private static boolean removeBackup(String name) { } private static void restoreInventoryIfNeeded(ServerPlayer player) { String name = player.getName().getString(); + File file = getBackupFile(name); CompoundTag backup = loadBackupInventory(name); - if (backup != null) { + if (backup != null && file != null) { // Capture items received while unauthenticated (e.g. starter kits or mod items) List newItems = new ArrayList<>(); for (int i = 0; i < player.getInventory().getContainerSize(); i++) { @@ -290,8 +305,15 @@ private static void restoreInventoryIfNeeded(ServerPlayer player) { } } - // Full NBT restore (supports mod data like Curios, etc) - player.load(backup); + boolean isFullNbt = file.getName().endsWith(".dat"); + if (isFullNbt && !config.inventoryOnly) { + player.load(backup); // Full NBT restore (supports mod data like Curios, etc) + } else { + // Inventory only restore (either from .inv file OR from .dat file if config changed) + if (backup.contains("Inventory", 9)) { + player.getInventory().load(backup.getList("Inventory", 10)); + } + } // Merge new items back in, dropping if inventory is full for (ItemStack stack : newItems) { From a68b78a5f15b6eecae93c78e68593f23f03ab2d2 Mon Sep 17 00:00:00 2001 From: Justin Widen Date: Fri, 2 Jan 2026 09:01:42 -0600 Subject: [PATCH 12/12] Config option mergeOnRestore Added 'mergeOnRestore' option to the config to control whether items received while unauthenticated are merged with the restored inventory or discarded. Updated README's as well. --- README.md | 6 ++++++ README_zh-CN.md | 6 ++++++ .../java/cn/alini/offlineauth/AuthConfig.java | 2 ++ .../alini/offlineauth/OfflineAuthHandler.java | 18 +++++++++++------- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f83b655..8e60c7d 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ Edit `config/offlineauth/config.json` to customize: - `autoLoginEnable` — Enable/disable auto-login feature. - `autoLoginExpireSeconds` — Time window for IP-based auto-login. - `messages` — Customize all prompts and warnings. +- `inventoryOnly` - Only backup player inventory data during auth. + - **False** (*Default*) | In this mode all player NBT data is backed up, including mod data like Curios, backpacks, etc. + - **True** | Only the player inventory data is backed up, this is faster and acceptable for Vanilla servers. +- `mergeOnRestore` - Merge items received while unauthenticated (e.g. Starter Kits) with the restored inventory. + - **True** (*Default*) | Items are merged. If inventory is full, items are dropped. **NOTE**: This should always be enabled if `inventoryOnly` is **True** otherwise dupe glitches are possible. + - **False** | Items received while unauthenticated are discarded. ## Security Notice diff --git a/README_zh-CN.md b/README_zh-CN.md index 28e1b82..6f6a73c 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -34,6 +34,12 @@ - `autoLoginEnable` — 是否开启自动登录功能 - `autoLoginExpireSeconds` — 自动登录时间窗口(秒) - `messages` — 所有提示/警告内容均可自定义 +- `inventoryOnly` — 仅在认证期间备份玩家背包数据。 + - **False** (*默认*) | 在此模式下备份所有玩家 NBT 数据,包括像 Curios、背包等 mod 数据。 + - **True** | 仅备份玩家背包数据,这在原版服务器上更快且可接受。 +- `mergeOnRestore` — 将未认证期间获得的物品(例如新手礼包)与恢复后的背包合并。 + - **True** (*默认*) | 物品将被合并;如果背包已满,物品将掉落。**注意**:如果 `inventoryOnly` 为 **True**,此项应始终启用,否则可能导致物品复制漏洞。 + - **False** | 未认证期间获得的物品将被丢弃。 ## 安全提醒 diff --git a/src/main/java/cn/alini/offlineauth/AuthConfig.java b/src/main/java/cn/alini/offlineauth/AuthConfig.java index 9281332..71031fa 100644 --- a/src/main/java/cn/alini/offlineauth/AuthConfig.java +++ b/src/main/java/cn/alini/offlineauth/AuthConfig.java @@ -21,6 +21,7 @@ public class AuthConfig { public boolean autoLoginEnable = true; public boolean failBlockEnable = true; public boolean inventoryOnly = false; // true = Only backup inventory | false = Backup full player NBT + public boolean mergeOnRestore = true; // true = Merge items received while unauthenticated | false = Overwrite public String prefix = "§7[§bAuth§7] "; public Map messages = new HashMap<>(); @@ -80,6 +81,7 @@ public void load() { this.failLockSeconds = loaded.failLockSeconds; this.autoLoginEnable = loaded.autoLoginEnable; this.inventoryOnly = loaded.inventoryOnly; + this.mergeOnRestore = loaded.mergeOnRestore; this.failBlockEnable = loaded.failBlockEnable; this.prefix = loaded.prefix; if (loaded.messages != null) this.messages.putAll(loaded.messages); diff --git a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java index fc385cf..76a1bcb 100644 --- a/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java +++ b/src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java @@ -298,10 +298,12 @@ private static void restoreInventoryIfNeeded(ServerPlayer player) { if (backup != null && file != null) { // Capture items received while unauthenticated (e.g. starter kits or mod items) List newItems = new ArrayList<>(); - for (int i = 0; i < player.getInventory().getContainerSize(); i++) { - ItemStack stack = player.getInventory().getItem(i); - if (!stack.isEmpty()) { - newItems.add(stack.copy()); + if (config.mergeOnRestore) { + for (int i = 0; i < player.getInventory().getContainerSize(); i++) { + ItemStack stack = player.getInventory().getItem(i); + if (!stack.isEmpty()) { + newItems.add(stack.copy()); + } } } @@ -316,9 +318,11 @@ private static void restoreInventoryIfNeeded(ServerPlayer player) { } // Merge new items back in, dropping if inventory is full - for (ItemStack stack : newItems) { - if (!player.getInventory().add(stack)) { - player.drop(stack, false); + if (config.mergeOnRestore) { + for (ItemStack stack : newItems) { + if (!player.getInventory().add(stack)) { + player.drop(stack, false); + } } }