diff --git a/README.md b/README.md index 184112e..8e60c7d 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 @@ -33,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 df58081..6f6a73c 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -22,6 +22,7 @@ - `/login <密码>` — 登录账户 - `/changepassword <旧密码> <新密码>` — 修改密码 - `/auth help` — 查看帮助信息 +- `/auth reload` — 重载配置文件(需要权限等级 2) ## 配置说明 @@ -33,6 +34,12 @@ - `autoLoginEnable` — 是否开启自动登录功能 - `autoLoginExpireSeconds` — 自动登录时间窗口(秒) - `messages` — 所有提示/警告内容均可自定义 +- `inventoryOnly` — 仅在认证期间备份玩家背包数据。 + - **False** (*默认*) | 在此模式下备份所有玩家 NBT 数据,包括像 Curios、背包等 mod 数据。 + - **True** | 仅备份玩家背包数据,这在原版服务器上更快且可接受。 +- `mergeOnRestore` — 将未认证期间获得的物品(例如新手礼包)与恢复后的背包合并。 + - **True** (*默认*) | 物品将被合并;如果背包已满,物品将掉落。**注意**:如果 `inventoryOnly` 为 **True**,此项应始终启用,否则可能导致物品复制漏洞。 + - **False** | 未认证期间获得的物品将被丢弃。 ## 安全提醒 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..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.1 +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..71031fa 100644 --- a/src/main/java/cn/alini/offlineauth/AuthConfig.java +++ b/src/main/java/cn/alini/offlineauth/AuthConfig.java @@ -20,6 +20,8 @@ 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 boolean mergeOnRestore = true; // true = Merge items received while unauthenticated | false = Overwrite public String prefix = "§7[§bAuth§7] "; public Map messages = new HashMap<>(); @@ -55,6 +57,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() @@ -77,6 +80,8 @@ public void load() { this.maxFailAttempts = loaded.maxFailAttempts; 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 c747775..76a1bcb 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,10 +32,11 @@ @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<>(); - 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<>(); @@ -44,7 +47,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)); @@ -129,11 +132,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 +160,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,59 +238,101 @@ 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(); + 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 + ".json"); + File file = new File(dir, name + ext); 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 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) { - File file = new File(INVENTORY_DIR, name + ".json"); - return file.exists(); + return getBackupFile(name) != null; } - 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 + ".json"); - 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); - 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; } } - 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(); + 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) {} + } + return !file.exists(); } private static void restoreInventoryIfNeeded(ServerPlayer player) { String name = player.getName().getString(); - ItemStack[] backup = loadBackupInventory(name, player.getInventory().getContainerSize()); - if (backup != null) { - for (int i = 0; i < backup.length; i++) { - player.getInventory().setItem(i, backup[i]); + File file = getBackupFile(name); + CompoundTag backup = loadBackupInventory(name); + if (backup != null && file != null) { + // Capture items received while unauthenticated (e.g. starter kits or mod items) + List newItems = new ArrayList<>(); + 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()); + } + } } - removeBackup(name); + + 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 + if (config.mergeOnRestore) { + 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); + 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(); + player.containerMenu.broadcastChanges(); player.sendSystemMessage(Component.literal(config.msg("inventory_restored"))); } } @@ -546,6 +590,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 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(); } }