Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Supports registration, login, inventory protection, brute-force prevention, and
- `/login <password>` — Login to your account.
- `/changepassword <old> <new>` — Change your password.
- `/auth help` — Show help information.
- `/auth reload` — Reload the configuration file (requires permission level 2).

## Configuration

Expand All @@ -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

Expand Down
7 changes: 7 additions & 0 deletions README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- `/login <密码>` — 登录账户
- `/changepassword <旧密码> <新密码>` — 修改密码
- `/auth help` — 查看帮助信息
- `/auth reload` — 重载配置文件(需要权限等级 2)

## 配置说明

Expand All @@ -33,6 +34,12 @@
- `autoLoginEnable` — 是否开启自动登录功能
- `autoLoginExpireSeconds` — 自动登录时间窗口(秒)
- `messages` — 所有提示/警告内容均可自定义
- `inventoryOnly` — 仅在认证期间备份玩家背包数据。
- **False** (*默认*) | 在此模式下备份所有玩家 NBT 数据,包括像 Curios、背包等 mod 数据。
- **True** | 仅备份玩家背包数据,这在原版服务器上更快且可接受。
- `mergeOnRestore` — 将未认证期间获得的物品(例如新手礼包)与恢复后的背包合并。
- **True** (*默认*) | 物品将被合并;如果背包已满,物品将掉落。**注意**:如果 `inventoryOnly` 为 **True**,此项应始终启用,否则可能导致物品复制漏洞。
- **False** | 未认证期间获得的物品将被丢弃。

## 安全提醒

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/cn/alini/offlineauth/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> messages = new HashMap<>();

Expand Down Expand Up @@ -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()
Expand All @@ -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);
Expand Down
128 changes: 90 additions & 38 deletions src/main/java/cn/alini/offlineauth/OfflineAuthHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@
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;
import java.util.*;

@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<String> loggedIn = new HashSet<>();
private static final Map<String, ItemStack[]> inventoryBackup = new HashMap<>();
private static final Map<String, CompoundTag> inventoryBackup = new HashMap<>();
private static final Map<String, Long> joinTimeMap = new HashMap<>();
private static final Map<String, Integer> notLoggedTick = new HashMap<>();
private static final Map<String, double[]> notLoggedSpawnPos = new HashMap<>();
Expand All @@ -44,7 +47,7 @@ public class OfflineAuthHandler {
private static final Gson gson = new Gson();
private static final Map<String, AutoLoginInfo> autoLoginMap = new HashMap<>();
private static final Map<String, FailInfo> 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));
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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")));
Expand Down Expand Up @@ -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<ItemStack> 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")));
}
}
Expand Down Expand Up @@ -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;
})
)
);
}
}
1 change: 0 additions & 1 deletion src/main/java/cn/alini/offlineauth/Offlineauth.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ public class Offlineauth {
public Offlineauth() {
//mod成功加载日志
LOGGER.info("OfflineAuth mod loaded successfully!");
new AuthConfig().save();
}
}