diff --git a/build.gradle b/build.gradle index b4b886938..97f204521 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.9.4-beta3.2" +version = "1.9.4-beta3.3" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/src/main/java/kamkeel/npcs/command/profile/CommandProfile.java b/src/main/java/kamkeel/npcs/command/profile/CommandProfile.java index 035fecb1a..70607f4f2 100644 --- a/src/main/java/kamkeel/npcs/command/profile/CommandProfile.java +++ b/src/main/java/kamkeel/npcs/command/profile/CommandProfile.java @@ -99,7 +99,8 @@ public class CommandProfile extends CommandBase { if(args.length <= useArgs.length + 2) { if(args.length - 3 >= 0){ String usage = useArgs[args.length - 3]; - if(usage.equals("") || usage.equals("[player]")) { + if(usage.equals("") || usage.equals("[player]") || usage.equals("") + || usage.equals("") || usage.equals("")) { return CommandBase.getListOfStringsMatchingLastWord(args, MinecraftServer.getServer().getAllUsernames()); } } diff --git a/src/main/java/kamkeel/npcs/command/profile/CommandProfileAdmin.java b/src/main/java/kamkeel/npcs/command/profile/CommandProfileAdmin.java index 13b4a13c5..7f768d480 100644 --- a/src/main/java/kamkeel/npcs/command/profile/CommandProfileAdmin.java +++ b/src/main/java/kamkeel/npcs/command/profile/CommandProfileAdmin.java @@ -4,14 +4,13 @@ import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.data.Profile; import kamkeel.npcs.controllers.data.ProfileOperation; import kamkeel.npcs.controllers.data.Slot; +import noppes.npcs.LogWriter; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; -import noppes.npcs.LogWriter; -import net.minecraft.nbt.NBTTagCompound; +import java.io.File; import java.util.Map; import java.util.UUID; - import static kamkeel.npcs.util.ColorUtil.*; public class CommandProfileAdmin extends CommandProfileBase { @@ -23,7 +22,7 @@ public class CommandProfileAdmin extends CommandProfileBase { @Override public String getDescription() { - return "Admin profile management: clone, remove, change, and list a player's slots."; + return "Admin profile management: clone, remove, change, list, rename, and rollback profiles."; } @Override @@ -36,7 +35,7 @@ public class CommandProfileAdmin extends CommandProfileBase { usage = " [temp]" ) public void clone(ICommandSender sender, String[] args) throws CommandException { - if (args.length < 4) { + if(args.length < 4) { sendError(sender, "Usage: /profile admin clone [temp]"); return; } @@ -47,55 +46,31 @@ public class CommandProfileAdmin extends CommandProfileBase { sourceSlot = Integer.parseInt(args[2]); destinationSlot = Integer.parseInt(args[3]); } catch (NumberFormatException ex) { - sendError(sender, "Source and destination slot IDs must be numbers."); + sendError(sender, "Slot IDs must be numbers."); return; } boolean temporary = (args.length >= 5 && args[4].equalsIgnoreCase("temp")); - - Profile sourceProfile = ProfileController.getProfile(sourcePlayer); - if (sourceProfile == null) { - sendError(sender, "Source profile not found for player: " + sourcePlayer); - return; - } - Profile destProfile = ProfileController.getProfile(destinationPlayer); - if (destProfile == null) { - sendError(sender, "Destination profile not found for player: " + destinationPlayer); - return; - } - if (destProfile.locked) { - sendError(sender, "Destination profile is locked. Please try again later."); - return; - } - if (!sourceProfile.slots.containsKey(sourceSlot)) { - sendError(sender, "Source slot " + sourceSlot + " does not exist for player: " + sourcePlayer); - return; - } - if (destinationSlot == destProfile.currentID) { - sendError(sender, "Cannot clone into the destination's active slot."); - return; - } - try { - Slot sourceSlotObj = sourceProfile.slots.get(sourceSlot); - if (sourceSlotObj == null) { - sendError(sender, "Source slot is invalid."); - return; - } - NBTTagCompound clonedData = (NBTTagCompound) sourceSlotObj.getCompound().copy(); - Slot clonedSlot = new Slot(destinationSlot, "Admin Cloned Slot " + destinationSlot, clonedData, System.currentTimeMillis(), temporary); - destProfile.slots.put(destinationSlot, clonedSlot); - - if (destProfile.player != null) { - ProfileController.save(destProfile.player, destProfile); - } else { - UUID destUUID = ProfileController.getUUIDFromUsername(destinationPlayer); - if (destUUID != null) - ProfileController.saveOffline(destProfile, destUUID); - } - sendResult(sender, "Successfully cloned slot %d from %s to slot %d for %s%s.", - sourceSlot, sourcePlayer, destinationSlot, destinationPlayer, (temporary ? " (temporary)" : "")); - } catch (Exception e) { - LogWriter.error("Error cloning slot", e); - sendError(sender, "An error occurred while cloning slot."); + ProfileOperation result = ProfileController.cloneSlot(sourcePlayer, sourceSlot, destinationSlot, temporary); + switch(result) { + case SUCCESS: + sendResult(sender, "Successfully cloned slot %d from %s to slot %d for %s.", sourceSlot, sourcePlayer, destinationSlot, destinationPlayer); + break; + case NEW_SLOT_CREATED: + sendResult(sender, "Successfully cloned slot %d from %s to NEW slot %d for %s.", sourceSlot, sourcePlayer, destinationSlot, destinationPlayer); + break; + case LOCKED: + sendError(sender, "Profile is locked. Please try again later."); + break; + case VERIFICATION_FAILED: + sendError(sender, "Profile verification failed. Change not permitted."); + break; + case PLAYER_NOT_FOUND: + sendError(sender, "Player profile not found."); + break; + case ERROR: + default: + sendError(sender, "Error cloning slot."); + break; } } @@ -104,7 +79,7 @@ public class CommandProfileAdmin extends CommandProfileBase { usage = " " ) public void remove(ICommandSender sender, String[] args) throws CommandException { - if (args.length < 2) { + if(args.length < 2) { sendError(sender, "Usage: /profile admin remove "); return; } @@ -112,29 +87,21 @@ public class CommandProfileAdmin extends CommandProfileBase { int slotId; try { slotId = Integer.parseInt(args[1]); - } catch (NumberFormatException ex) { + } catch(NumberFormatException ex) { sendError(sender, "Slot ID must be a number."); return; } - Profile targetProfile = ProfileController.getProfile(targetPlayer); - if (targetProfile == null) { - sendError(sender, "Profile not found for player: " + targetPlayer); - return; - } - if (targetProfile.locked) { - sendError(sender, "Profile for " + targetPlayer + " is locked. Please try again later."); - return; - } ProfileOperation result = ProfileController.removeSlot(targetPlayer, slotId); - switch (result) { + switch(result) { case SUCCESS: sendResult(sender, "Successfully removed slot %d from %s.", slotId, targetPlayer); break; case LOCKED: - sendError(sender, "Profile for " + targetPlayer + " is locked."); + sendError(sender, "Profile for %s is locked. Please try again later.", targetPlayer); break; default: sendError(sender, "Error removing slot %d from %s.", slotId, targetPlayer); + break; } } @@ -143,7 +110,7 @@ public class CommandProfileAdmin extends CommandProfileBase { usage = " " ) public void change(ICommandSender sender, String[] args) throws CommandException { - if (args.length < 2) { + if(args.length < 2) { sendError(sender, "Usage: /profile admin change "); return; } @@ -151,29 +118,29 @@ public class CommandProfileAdmin extends CommandProfileBase { int newSlotId; try { newSlotId = Integer.parseInt(args[1]); - } catch (NumberFormatException ex) { + } catch(NumberFormatException ex) { sendError(sender, "Slot ID must be a number."); return; } - Profile targetProfile = ProfileController.getProfile(targetPlayer); - if (targetProfile == null) { - sendError(sender, "Profile not found for player: " + targetPlayer); - return; - } - if (targetProfile.locked) { - sendError(sender, "Profile for " + targetPlayer + " is locked. Please try again later."); - return; - } ProfileOperation result = ProfileController.changeSlot(targetPlayer, newSlotId); - switch (result) { + switch(result) { case SUCCESS: sendResult(sender, "Successfully changed %s's active slot to %d.", targetPlayer, newSlotId); break; + case NEW_SLOT_CREATED: + sendResult(sender, "Switched %s to NEW slot %d (new profile created).", targetPlayer, newSlotId); + break; + case ALREADY_ACTIVE: + sendError(sender, "Slot %d is already active for %s.", newSlotId, targetPlayer); + break; case LOCKED: - sendError(sender, "Profile for " + targetPlayer + " is locked."); + sendError(sender, "Profile for %s is locked. Please try again later.", targetPlayer); + break; + case VERIFICATION_FAILED: + sendError(sender, "Profile verification failed for %s.", targetPlayer); break; default: - sendError(sender, "Error changing active slot for " + targetPlayer); + sendError(sender, "Error changing active slot for %s.", targetPlayer); } } @@ -182,21 +149,21 @@ public class CommandProfileAdmin extends CommandProfileBase { usage = "" ) public void list(ICommandSender sender, String[] args) throws CommandException { - if (args.length < 1) { + if(args.length < 1) { sendError(sender, "Usage: /profile admin list "); return; } String targetPlayer = args[0]; - Profile targetProfile = ProfileController.getProfile(targetPlayer); - if (targetProfile == null) { + Profile profile = ProfileController.getProfile(targetPlayer); + if(profile == null) { sendError(sender, "Profile not found for player: " + targetPlayer); return; } - sendMessage(sender, "Profile Slots for " + targetPlayer + ":"); - for (Map.Entry entry : targetProfile.slots.entrySet()) { + sendMessage(sender, "Profile Slots for %s:", targetPlayer); + for(Map.Entry entry : profile.slots.entrySet()) { int id = entry.getKey(); String name = entry.getValue().getName(); - String prefix = (id == targetProfile.currentID) ? "* " : "- "; + String prefix = (id == profile.currentID) ? "* " : "- "; sendMessage(sender, prefix + "Slot " + id + ": " + name); } } @@ -206,7 +173,7 @@ public class CommandProfileAdmin extends CommandProfileBase { usage = " " ) public void rename(ICommandSender sender, String[] args) throws CommandException { - if (args.length < 3) { + if(args.length < 3) { sendError(sender, "Usage: /profile admin rename "); return; } @@ -214,50 +181,69 @@ public class CommandProfileAdmin extends CommandProfileBase { int slotId; try { slotId = Integer.parseInt(args[1]); - } catch (NumberFormatException ex) { + } catch(NumberFormatException ex) { sendError(sender, "Slot ID must be a number."); return; } - // Combine remaining arguments for the new name. StringBuilder sb = new StringBuilder(); - for (int i = 2; i < args.length; i++) { - if (i > 2) sb.append(" "); + for(int i = 2; i < args.length; i++){ + if(i > 2) sb.append(" "); sb.append(args[i]); } String newName = sb.toString().trim(); - // Cap name at 20 characters. - if (newName.length() > 20) { + if(newName.length() > 20) { newName = newName.substring(0, 20); } - // Validate that name contains only letters and spaces. - if (!newName.matches("[A-Za-z ]+")) { + if(!newName.matches("[A-Za-z ]+")){ sendError(sender, "Invalid name. Only alphabetic characters and spaces are allowed."); return; } - Profile targetProfile = ProfileController.getProfile(targetPlayer); - if (targetProfile == null) { + Profile profile = ProfileController.getProfile(targetPlayer); + if(profile == null) { sendError(sender, "Profile not found for player: " + targetPlayer); return; } - if (targetProfile.locked) { - sendError(sender, "Profile for " + targetPlayer + " is locked. Please try again later."); + if(profile.locked) { + sendError(sender, "Profile for %s is locked. Please try again later.", targetPlayer); return; } - if (!targetProfile.slots.containsKey(slotId)) { - sendError(sender, "Slot " + slotId + " not found in " + targetPlayer + "'s profile."); + if(!profile.slots.containsKey(slotId)) { + sendError(sender, "Slot %d not found in %s's profile.", slotId, targetPlayer); return; } - // Rename the slot. - Slot targetSlot = targetProfile.slots.get(slotId); - targetSlot.setName(newName); - // Save the updated profile. - if (targetProfile.player != null) { - ProfileController.save(targetProfile.player, targetProfile); + profile.slots.get(slotId).setName(newName); + if(profile.player != null) { + ProfileController.save(profile.player, profile); } else { UUID uuid = ProfileController.getUUIDFromUsername(targetPlayer); - if (uuid != null) - ProfileController.saveOffline(targetProfile, uuid); + if(uuid != null) + ProfileController.saveOffline(profile, uuid); + } + sendResult(sender, "Successfully renamed slot %d for %s to '%s'.", slotId, targetPlayer, newName); + } + + @SubCommand( + desc = "Rollback a player's profile to one of their backups.", + usage = " " + ) + public void rollback(ICommandSender sender, String[] args) throws CommandException { + if(args.length < 2) { + sendError(sender, "Usage: /profile admin rollback "); + return; + } + String targetPlayer = args[0]; + String backupFileName = args[1]; + File backupDir = new File(ProfileController.getBackupDir(), ProfileController.getProfile(targetPlayer).player.getUniqueID().toString()); + File backupFile = new File(backupDir, backupFileName); + if(!backupFile.exists()){ + sendError(sender, "Backup file %s not found for player %s.", backupFileName, targetPlayer); + return; + } + boolean success = ProfileController.rollbackProfile(targetPlayer, backupFile); + if(success) { + sendResult(sender, "Successfully rolled back %s's profile to backup %s.", targetPlayer, backupFileName); + } else { + sendError(sender, "Failed to rollback %s's profile.", targetPlayer); } - sendResult(sender, "Successfully renamed slot %s for %s to '%s'.", slotId, targetPlayer, newName); } } diff --git a/src/main/java/kamkeel/npcs/command/profile/CommandProfileChange.java b/src/main/java/kamkeel/npcs/command/profile/CommandProfileChange.java index d58660714..8f8c46fbc 100644 --- a/src/main/java/kamkeel/npcs/command/profile/CommandProfileChange.java +++ b/src/main/java/kamkeel/npcs/command/profile/CommandProfileChange.java @@ -5,7 +5,6 @@ import kamkeel.npcs.controllers.data.ProfileOperation; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayer; - import static kamkeel.npcs.util.ColorUtil.sendError; import static kamkeel.npcs.util.ColorUtil.sendResult; @@ -33,35 +32,41 @@ public class CommandProfileChange extends CommandProfileBase { @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { - if (!(sender instanceof EntityPlayer)) { + if(!(sender instanceof EntityPlayer)) { sendError(sender, "This command can only be used by a player."); return; } - if (args.length < 1) { + if(args.length < 1) { sendError(sender, "Usage: " + getUsage()); return; } int slotId; try { slotId = Integer.parseInt(args[0]); - } catch (NumberFormatException ex) { + } catch(NumberFormatException ex) { sendError(sender, "Slot ID must be a number: " + args[0]); return; } - EntityPlayer player = (EntityPlayer) sender; + EntityPlayer player = (EntityPlayer)sender; ProfileOperation result = ProfileController.changeSlot(player, slotId); switch(result) { case SUCCESS: - sendResult(sender, "Successfully changed profile slot to %s.", slotId); + sendResult(sender, "Successfully changed profile slot to %d.", slotId); + break; + case NEW_SLOT_CREATED: + sendResult(sender, "Switched to new slot %d (new profile created).", slotId); + break; + case ALREADY_ACTIVE: + sendError(sender, "Slot %d is already active.", slotId); break; case LOCKED: sendError(sender, "Profile is locked. Please try again later."); break; - case ERROR: - sendError(sender, "Error occurred while changing your profile slot."); + case VERIFICATION_FAILED: + sendError(sender, "Profile verification failed. Change not permitted."); break; default: - sendError(sender, "Operation could not be completed."); + sendError(sender, "Error occurred while changing your profile slot."); } } } diff --git a/src/main/java/kamkeel/npcs/command/profile/CommandProfileList.java b/src/main/java/kamkeel/npcs/command/profile/CommandProfileList.java index 97b6bbded..07d03fc90 100644 --- a/src/main/java/kamkeel/npcs/command/profile/CommandProfileList.java +++ b/src/main/java/kamkeel/npcs/command/profile/CommandProfileList.java @@ -6,9 +6,7 @@ import kamkeel.npcs.controllers.data.Slot; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayer; - import java.util.Map; - import static kamkeel.npcs.util.ColorUtil.sendError; import static kamkeel.npcs.util.ColorUtil.sendMessage; @@ -31,18 +29,21 @@ public class CommandProfileList extends CommandProfileBase { @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { - if (!(sender instanceof EntityPlayer)) { + if(!(sender instanceof EntityPlayer)) { sendError(sender, "This command can only be used by a player."); return; } - EntityPlayer player = (EntityPlayer) sender; + EntityPlayer player = (EntityPlayer)sender; Profile profile = ProfileController.getProfile(player); - if (profile == null) { + if(profile == null) { sendError(sender, "Profile not found."); return; } + if(profile.slots.isEmpty()){ + sendMessage(sender, "No slots found. Using default slot 0."); + } sendMessage(sender, "Your Profile Slots:"); - for (Map.Entry entry : profile.slots.entrySet()) { + for(Map.Entry entry : profile.slots.entrySet()) { int id = entry.getKey(); String name = entry.getValue().getName(); String prefix = (id == profile.currentID) ? "* " : "- "; diff --git a/src/main/java/kamkeel/npcs/command/profile/CommandProfileRemove.java b/src/main/java/kamkeel/npcs/command/profile/CommandProfileRemove.java index 1d8b53413..81d7e6a4b 100644 --- a/src/main/java/kamkeel/npcs/command/profile/CommandProfileRemove.java +++ b/src/main/java/kamkeel/npcs/command/profile/CommandProfileRemove.java @@ -5,7 +5,6 @@ import kamkeel.npcs.controllers.data.ProfileOperation; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayer; - import static kamkeel.npcs.util.ColorUtil.sendError; import static kamkeel.npcs.util.ColorUtil.sendResult; @@ -33,22 +32,22 @@ public class CommandProfileRemove extends CommandProfileBase { @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { - if (!(sender instanceof EntityPlayer)) { + if(!(sender instanceof EntityPlayer)) { sendError(sender, "This command can only be used by a player."); return; } - if (args.length < 1) { + if(args.length < 1) { sendError(sender, "Usage: " + getUsage()); return; } int slotId; try { slotId = Integer.parseInt(args[0]); - } catch (NumberFormatException ex) { + } catch(NumberFormatException ex) { sendError(sender, "Slot ID must be a number: " + args[0]); return; } - EntityPlayer player = (EntityPlayer) sender; + EntityPlayer player = (EntityPlayer)sender; ProfileOperation result = ProfileController.removeSlot(player, slotId); switch(result) { case SUCCESS: diff --git a/src/main/java/kamkeel/npcs/command/profile/CommandProfileRename.java b/src/main/java/kamkeel/npcs/command/profile/CommandProfileRename.java index 4155427c1..dbb116d87 100644 --- a/src/main/java/kamkeel/npcs/command/profile/CommandProfileRename.java +++ b/src/main/java/kamkeel/npcs/command/profile/CommandProfileRename.java @@ -1,12 +1,10 @@ package kamkeel.npcs.command.profile; import kamkeel.npcs.controllers.ProfileController; -import kamkeel.npcs.controllers.data.Profile; -import kamkeel.npcs.controllers.data.Slot; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayer; - +import java.util.UUID; import static kamkeel.npcs.util.ColorUtil.sendError; import static kamkeel.npcs.util.ColorUtil.sendResult; @@ -29,52 +27,45 @@ public class CommandProfileRename extends CommandProfileBase { @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { - if (!(sender instanceof EntityPlayer)) { + if(!(sender instanceof EntityPlayer)) { sendError(sender, "This command can only be used by a player."); return; } - if (args.length < 2) { + if(args.length < 2) { sendError(sender, "Usage: " + getUsage()); return; } int slotId; try { slotId = Integer.parseInt(args[0]); - } catch (NumberFormatException e) { + } catch(NumberFormatException ex) { sendError(sender, "Slot ID must be a number."); return; } - // Combine remaining arguments into one name string. StringBuilder sb = new StringBuilder(); - for (int i = 1; i < args.length; i++) { - if (i > 1) sb.append(" "); + for(int i = 1; i < args.length; i++){ + if(i > 1) sb.append(" "); sb.append(args[i]); } String newName = sb.toString().trim(); - // Cap at 20 characters. - if (newName.length() > 20) { + if(newName.length() > 20) { newName = newName.substring(0, 20); } - // Validate that the new name contains only letters and spaces. - if (!newName.matches("[A-Za-z ]+")) { + if(!newName.matches("[A-Za-z ]+")){ sendError(sender, "Invalid name. Only alphabetic characters and spaces are allowed."); return; } - - EntityPlayer player = (EntityPlayer) sender; - Profile profile = ProfileController.getProfile(player); - if (profile == null) { + EntityPlayer player = (EntityPlayer)sender; + if(ProfileController.getProfile(player) == null) { sendError(sender, "Profile not found."); return; } - if (!profile.slots.containsKey(slotId)) { - sendError(sender, "Slot " + slotId + " not found in your profile."); + if(!ProfileController.getProfile(player).slots.containsKey(slotId)) { + sendError(sender, "Slot %d not found in your profile.", slotId); return; } - Slot slot = profile.slots.get(slotId); - slot.setName(newName); - // Save the profile after renaming. - ProfileController.save(player, profile); - sendResult(sender, "Successfully renamed slot %s to '%s'.", slotId, newName); + ProfileController.getProfile(player).slots.get(slotId).setName(newName); + ProfileController.save(player, ProfileController.getProfile(player)); + sendResult(sender, "Successfully renamed slot %d to '%s'.", slotId, newName); } } diff --git a/src/main/java/kamkeel/npcs/controllers/ProfileController.java b/src/main/java/kamkeel/npcs/controllers/ProfileController.java index 0c5f74427..d8a316ba3 100644 --- a/src/main/java/kamkeel/npcs/controllers/ProfileController.java +++ b/src/main/java/kamkeel/npcs/controllers/ProfileController.java @@ -1,8 +1,8 @@ package kamkeel.npcs.controllers; import kamkeel.npcs.controllers.data.IProfileData; -import kamkeel.npcs.controllers.data.ProfileOperation; import kamkeel.npcs.controllers.data.Profile; +import kamkeel.npcs.controllers.data.ProfileOperation; import kamkeel.npcs.controllers.data.Slot; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; @@ -11,17 +11,15 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.server.MinecraftServer; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigMain; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.util.CustomNPCsThreader; import noppes.npcs.util.NBTJsonUtil; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.IOException; +import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.text.SimpleDateFormat; import java.util.*; public class ProfileController { @@ -36,7 +34,6 @@ public class ProfileController { } // ---------- Registration & I/O ---------- - public static boolean registerProfileType(IProfileData type){ if(profileTypes.containsKey(type.getTagName())) return false; @@ -56,7 +53,18 @@ public class ProfileController { } } - // Login for online players + // Returns the backup folder for profiles. + public static File getBackupDir() { + File base = getProfileDir(); + File backup = new File(base, "backup"); + if(!backup.exists()){ + backup.mkdir(); + } + return backup; + } + + // Login for online players. + // If no slots exist, create default slot 0 and save the player's current IProfileData into it. public static synchronized void login(EntityPlayer player){ if(player == null) return; @@ -67,6 +75,15 @@ public class ProfileController { } else { NBTTagCompound compound = load(player); profile = new Profile(player, compound); + // If no slots exist, create default slot 0. + if(profile.slots.isEmpty()){ + Slot defaultSlot = new Slot(0, "Default Slot"); + defaultSlot.setCompound(new NBTTagCompound()); + profile.slots.put(0, defaultSlot); + profile.currentID = 0; + saveSlotData(player); + save(player, profile); + } activeProfiles.put(player.getUniqueID(), profile); } } @@ -99,7 +116,7 @@ public class ProfileController { return new NBTTagCompound(); } - // Offline save (synchronous) when no EntityPlayer is available + // Offline save (synchronous) when no EntityPlayer is available. public static synchronized void saveOffline(Profile profile, UUID uuid) { final NBTTagCompound compound = profile.writeToNBT(); final String filename = uuid.toString() + ".dat"; @@ -112,12 +129,14 @@ public class ProfileController { fileOld.delete(); } fileNew.renameTo(fileOld); + // Also backup if allowed. + backupProfile(uuid, compound); } catch (Exception e) { LogWriter.except(e); } } - // Save for online players (asynchronous) + // Save for online players (asynchronous). public static synchronized void save(EntityPlayer player, Profile profile) { profile.locked = true; CustomNPCsThreader.customNPCThread.execute(() -> { @@ -132,6 +151,10 @@ public class ProfileController { fileOld.delete(); } fileNew.renameTo(fileOld); + // Backup if enabled. + if(ConfigMain.AllowProfileBackups) { + backupProfile(player.getUniqueID(), compound); + } } catch (Exception e) { LogWriter.except(e); } finally { @@ -140,7 +163,67 @@ public class ProfileController { }); } - // Logout removes an active profile + // Backup the profile by saving a copy to profiles/backup//.dat. + private static void backupProfile(UUID uuid, NBTTagCompound compound) { + try { + File backupDir = new File(getBackupDir(), uuid.toString()); + if(!backupDir.exists()){ + backupDir.mkdirs(); + } + String dateStr = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + File backupFile = new File(backupDir, dateStr + ".dat"); + CompressedStreamTools.writeCompressed(compound, new FileOutputStream(backupFile)); + // Now enforce backup amount limit. + File[] backups = backupDir.listFiles((dir, name) -> name.endsWith(".dat")); + if(backups != null && backups.length > ConfigMain.ProfileBackupAmount) { + // Sort backups by last modified ascending (oldest first) + Arrays.sort(backups, Comparator.comparingLong(File::lastModified)); + for(int i = 0; i < backups.length - ConfigMain.ProfileBackupAmount; i++){ + backups[i].delete(); + } + } + } catch(Exception e) { + LogWriter.except(e); + } + } + + // Rollback a player's profile from a backup file. + // Returns true if successful. + public static boolean rollbackProfile(String username, File backupFile) { + UUID uuid = getUUIDFromUsername(username); + if(uuid == null) return false; + try (FileInputStream fis = new FileInputStream(backupFile)) { + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + // Overwrite the main profile file. + File saveDir = getProfileDir(); + File mainFile = new File(saveDir, uuid.toString() + ".dat"); + File fileNew = new File(saveDir, uuid.toString() + "_new"); + CompressedStreamTools.writeCompressed(compound, new FileOutputStream(fileNew)); + if(mainFile.exists()){ + mainFile.delete(); + } + fileNew.renameTo(mainFile); + + // Update the in-memory profile. + EntityPlayerMP player = getPlayer(username); + Profile newProfile; + if(player != null) { + newProfile = new Profile(player, compound); + activeProfiles.put(uuid, newProfile); + // Load the rolled-back data into the player's IProfileData. + loadSlotData(player); + } else { + newProfile = new Profile(null, compound); + activeProfiles.put(uuid, newProfile); + } + return true; + } catch(Exception e) { + LogWriter.except(e); + return false; + } + } + + // Logout removes an active profile. public static synchronized void logout(EntityPlayer player) { if(player != null) { activeProfiles.remove(player.getUniqueID()); @@ -148,8 +231,6 @@ public class ProfileController { } // ---------- Profile Retrieval Helpers ---------- - - // Get profile via EntityPlayer (online) public static Profile getProfile(EntityPlayer player) { if(player == null) return null; if(!activeProfiles.containsKey(player.getUniqueID())) @@ -157,7 +238,6 @@ public class ProfileController { return activeProfiles.get(player.getUniqueID()); } - // Get profile via UUID (may be offline) public static Profile getProfile(UUID uuid) { if(activeProfiles.containsKey(uuid)) return activeProfiles.get(uuid); @@ -165,7 +245,6 @@ public class ProfileController { return new Profile(null, compound); } - // Get profile via username (tries online first, then offline via Mojang API) public static Profile getProfile(String username) { EntityPlayer player = getPlayer(username); if(player != null) { @@ -178,7 +257,6 @@ public class ProfileController { } // ---------- Mojang API UUID Lookup ---------- - public static UUID getUUIDFromUsername(String username) { try { URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + username); @@ -214,7 +292,6 @@ public class ProfileController { } // ---------- Internal Slot Operation Helpers ---------- - // Returns the next available temporary (negative) slot id (starting at -1) private static int getNextAvailableTempSlot(Profile profile) { int id = -1; @@ -225,15 +302,12 @@ public class ProfileController { } // Clone a slot within a Profile. - // sourceSlotId is the slot to copy. - // destinationSlotId is the target id for non-temporary clones. // If temporary==true, destinationSlotId is computed automatically. private static ProfileOperation cloneSlotInternal(Profile profile, int sourceSlotId, int destinationSlotId, boolean temporary) { if(profile.locked) { LogWriter.error("Profile is locked; cannot clone slot."); return ProfileOperation.LOCKED; } - // Run verification checks on all IProfileData in priority order. if(profile.player != null) { List dataList = new ArrayList<>(profileTypes.values()); @@ -245,7 +319,6 @@ public class ProfileController { } } } - if(!profile.slots.containsKey(sourceSlotId)) { LogWriter.error("Source slot " + sourceSlotId + " does not exist."); return ProfileOperation.ERROR; @@ -288,17 +361,22 @@ public class ProfileController { } // Change the active slot in a Profile. + // If the new slot doesn't exist, it is created and NEW_SLOT_CREATED is returned. // Saves current slot data before switching. private static ProfileOperation changeSlotInternal(Profile profile, int newSlotId) { if(profile.locked) { LogWriter.error("Profile is locked; cannot change slot."); return ProfileOperation.LOCKED; } - - // Run verification checks from all registered IProfileData in order of priority. + // Disallow switching to the same active slot. + if(profile.currentID == newSlotId) { + LogWriter.error("Slot " + newSlotId + " is already active."); + return ProfileOperation.ALREADY_ACTIVE; + } + // Run verification checks from all IProfileData in order of priority. if(profile.player != null) { List dataList = new ArrayList<>(profileTypes.values()); - dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority));; + dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority)); for(IProfileData pd : dataList) { if(!pd.verifySwitch(profile.player)) { LogWriter.error("Verification check failed for profile type: " + pd.getTagName()); @@ -306,34 +384,31 @@ public class ProfileController { } } } - if(profile.player != null) { saveSlotData(profile.player); } - + boolean createdNewSlot = false; // If the new slot doesn't exist, create it. if(!profile.slots.containsKey(newSlotId)) { Slot newSlot = new Slot(newSlotId, "Slot " + newSlotId); newSlot.setCompound(new NBTTagCompound()); profile.slots.put(newSlotId, newSlot); + createdNewSlot = true; } - profile.currentID = newSlotId; if(profile.player != null) { loadSlotData(profile.player); } - // Update PlayerData so that PlayerData.get(player).profileSlot matches Profile.currentID. if(profile.player != null) { PlayerData pdata = PlayerData.get(profile.player); pdata.profileSlot = newSlotId; pdata.save(); } - return ProfileOperation.SUCCESS; + return createdNewSlot ? ProfileOperation.NEW_SLOT_CREATED : ProfileOperation.SUCCESS; } // ---------- Public Operations (Overloaded for EntityPlayer, UUID, or username) ---------- - // ----- Clone Slot ----- public static ProfileOperation cloneSlot(EntityPlayer player, int sourceSlotId, int destinationSlotId, boolean temporary) { Profile profile = getProfile(player); @@ -417,7 +492,7 @@ public class ProfileController { Profile profile = getProfile(player); if(profile == null) return ProfileOperation.PLAYER_NOT_FOUND; ProfileOperation result = changeSlotInternal(profile, newSlotId); - if(result == ProfileOperation.SUCCESS && player != null) + if((result == ProfileOperation.SUCCESS || result == ProfileOperation.NEW_SLOT_CREATED) && player != null) save(player, profile); return result; } @@ -426,7 +501,7 @@ public class ProfileController { Profile profile = getProfile(uuid); if(profile == null) return ProfileOperation.PLAYER_NOT_FOUND; ProfileOperation result = changeSlotInternal(profile, newSlotId); - if(result == ProfileOperation.SUCCESS) { + if(result == ProfileOperation.SUCCESS || result == ProfileOperation.NEW_SLOT_CREATED) { if(profile.player != null) save(profile.player, profile); else @@ -439,7 +514,7 @@ public class ProfileController { Profile profile = getProfile(username); if(profile == null) return ProfileOperation.PLAYER_NOT_FOUND; ProfileOperation result = changeSlotInternal(profile, newSlotId); - if(result == ProfileOperation.SUCCESS) { + if(result == ProfileOperation.SUCCESS || result == ProfileOperation.NEW_SLOT_CREATED) { if(profile.player != null) save(profile.player, profile); else { @@ -470,7 +545,6 @@ public class ProfileController { dataCompound = new NBTTagCompound(); slot.setCompound(dataCompound); } - List dataList = new ArrayList<>(profileTypes.values()); dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority)); for(IProfileData profileData : dataList){ @@ -483,15 +557,10 @@ public class ProfileController { public static void loadSlotData(EntityPlayer player) { if(player == null || !activeProfiles.containsKey(player.getUniqueID())) return; - Profile profile = activeProfiles.get(player.getUniqueID()); if(profile.slots.isEmpty() || !profile.slots.containsKey(profile.currentID)) return; - - // Get a copy of the active slot's NBT compound. NBTTagCompound slotCompound = (NBTTagCompound) profile.slots.get(profile.currentID).getCompound().copy(); - - // For each registered profile data type, load its saved compound onto the player. List dataList = new ArrayList<>(profileTypes.values()); dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority)); for(IProfileData profileData : dataList){ @@ -500,10 +569,8 @@ public class ProfileController { data = slotCompound.getCompoundTag(profileData.getTagName()); else data = new NBTTagCompound(); - profileData.setNBT(player, data); } - for(IProfileData profileData : dataList){ profileData.save(player); } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 1b936cd9b..a591a899b 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.data.LoginPacket; import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; import kamkeel.npcs.network.packets.data.large.SyncPacket; import kamkeel.npcs.network.enums.EnumSyncAction; @@ -23,6 +24,10 @@ import java.util.concurrent.ConcurrentHashMap; public class SyncController { public static void syncPlayer(EntityPlayerMP player){ + + // Login Packet + PacketHandler.Instance.sendToPlayer(new LoginPacket(), player); + // 1) Factions PacketHandler.Instance.sendToPlayer(new SyncPacket( EnumSyncType.FACTION, diff --git a/src/main/java/kamkeel/npcs/controllers/data/Profile.java b/src/main/java/kamkeel/npcs/controllers/data/Profile.java index 502bea91a..3a1baac89 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/Profile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/Profile.java @@ -11,9 +11,9 @@ public class Profile { public EntityPlayer player; public int currentID; public HashMap slots = new HashMap<>(); - public boolean locked = false; // Prevent modifications while saving + public boolean locked = false; - public Profile(EntityPlayer player, NBTTagCompound compound){ + public Profile(EntityPlayer player, NBTTagCompound compound) { this.player = player; if(compound.hasKey("CurrentID")){ this.currentID = compound.getInteger("CurrentID"); @@ -21,9 +21,9 @@ public class Profile { this.currentID = 0; } - if (compound.hasKey("Slots")) { + if(compound.hasKey("Slots")) { NBTTagCompound slotsCompound = compound.getCompoundTag("Slots"); - for (String key : (Set) slotsCompound.func_150296_c()) { + for(String key : (Set) slotsCompound.func_150296_c()){ int slotID = Integer.parseInt(key); NBTTagCompound slotNBT = slotsCompound.getCompoundTag(key); Slot slot = Slot.fromNBT(slotID, slotNBT); @@ -34,7 +34,6 @@ public class Profile { public NBTTagCompound writeToNBT() { NBTTagCompound fullCompound = new NBTTagCompound(); - if(this.player != null) fullCompound.setString("Name", player.getCommandSenderName()); fullCompound.setInteger("CurrentID", currentID); @@ -44,7 +43,7 @@ public class Profile { for (HashMap.Entry entry : slots.entrySet()) { Slot slot = entry.getValue(); if(slot.isTemporary()) - continue; // Do not save temporary slots + continue; int id = entry.getKey(); slot.setId(id); NBTTagCompound slotTag = slot.toNBT(); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ProfileOperation.java b/src/main/java/kamkeel/npcs/controllers/data/ProfileOperation.java index f3d6d1179..3e6e81b90 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ProfileOperation.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ProfileOperation.java @@ -5,6 +5,8 @@ public enum ProfileOperation { ERROR, LOCKED, PLAYER_NOT_FOUND, + NEW_SLOT_CREATED, + ALREADY_ACTIVE, VERIFICATION_FAILED } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 67003b53b..d8ca8da65 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -338,6 +338,7 @@ public class PacketHandler { public void registerDataPackets(){ // Data Packets + DATA_PACKET.registerPacket(new LoginPacket()); DATA_PACKET.registerPacket(new AchievementPacket()); DATA_PACKET.registerPacket(new ChatAlertPacket()); DATA_PACKET.registerPacket(new ChatBubblePacket()); diff --git a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java index 385b8fda4..022f4bc5e 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java @@ -1,21 +1,21 @@ package kamkeel.npcs.network.packets.data; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumDataPacket; -import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.config.ConfigMain; import java.io.IOException; public final class LoginPacket extends AbstractPacket { public static final String packetName = "CNPC+|Login"; - - public LoginPacket() { - } - + public LoginPacket() {} @Override public Enum getType() { @@ -29,13 +29,14 @@ public final class LoginPacket extends AbstractPacket { @Override public void sendData(ByteBuf out) throws IOException { -// ByteBufUtils.writeUTF8String(out, this.data.player.getCommandSenderName()); -// ByteBufUtils.writeNBT(out,this.data.saveFromNBT(new NBTTagCompound())); + out.writeBoolean(ConfigMain.PartiesEnabled); + out.writeBoolean(ConfigMain.ProfilesEnabled); } + @SideOnly(Side.CLIENT) @Override public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { - String playerName = ByteBufUtils.readUTF8String(in); - + ClientCacheHandler.allowParties = in.readBoolean(); + ClientCacheHandler.allowProfiles = in.readBoolean(); } } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 69463d914..699117a03 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -267,7 +267,8 @@ public class CustomNpcs { public void serverstart(FMLServerStartingEvent event) { event.registerServerCommand(new CommandNoppes()); event.registerServerCommand(new CommandKamkeel()); - event.registerServerCommand(new CommandProfile()); + if(ConfigMain.ProfilesEnabled) + event.registerServerCommand(new CommandProfile()); } private void registerNpc(Class cl, String name) { diff --git a/src/main/java/noppes/npcs/client/ClientCacheHandler.java b/src/main/java/noppes/npcs/client/ClientCacheHandler.java index 0661b7505..a51986c5d 100644 --- a/src/main/java/noppes/npcs/client/ClientCacheHandler.java +++ b/src/main/java/noppes/npcs/client/ClientCacheHandler.java @@ -23,6 +23,10 @@ public class ClientCacheHandler { public static Party party; + public static boolean allowProfiles = true; + public static boolean allowParties = true; + + public static ImageData getImageData(String directory) { synchronized (imageDataCache) { if (!imageDataCache.containsKey(directory)) { diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java index d2d3e3a01..68395a52f 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java @@ -1,10 +1,9 @@ package noppes.npcs.client.gui.player.inventory; -import cpw.mods.fml.common.Loader; -import kamkeel.npcs.addon.client.DBCClient; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.gui.util.GuiMenuSideButton; import noppes.npcs.client.gui.util.GuiNPCInterface; import noppes.npcs.config.ConfigClient; @@ -41,13 +40,14 @@ public class GuiCNPCInventory extends GuiNPCInterface { questsButton.renderResource = specialIcons; addButton(questsButton); - y += 21; - GuiMenuSideButton partyButton = new GuiMenuSideButton(-101, guiLeft + xSize + 37, this.guiTop + y, 22, 22, ""); - partyButton.rightSided = true; - partyButton.active = activeTab == -101; - partyButton.renderResource = specialIcons; - addButton(partyButton); - + if(ClientCacheHandler.allowParties){ + y += 21; + GuiMenuSideButton partyButton = new GuiMenuSideButton(-101, guiLeft + xSize + 37, this.guiTop + y, 22, 22, ""); + partyButton.rightSided = true; + partyButton.active = activeTab == -101; + partyButton.renderResource = specialIcons; + addButton(partyButton); + } if(ConfigClient.enableFactionTab){ y += 21; GuiMenuSideButton factionButton = new GuiMenuSideButton(-102, guiLeft + xSize + 37, this.guiTop + y, 22, 22, ""); @@ -57,7 +57,7 @@ public class GuiCNPCInventory extends GuiNPCInterface { factionButton.renderResource = specialIcons; addButton(factionButton); } - if(ConfigMain.EnableProfiles){ + if(ClientCacheHandler.allowProfiles){ y += 21; GuiMenuSideButton profileButton = new GuiMenuSideButton(-104, guiLeft + xSize + 37, this.guiTop + y, 22, 22, ""); profileButton.rightSided = true; diff --git a/src/main/java/noppes/npcs/config/ConfigMain.java b/src/main/java/noppes/npcs/config/ConfigMain.java index e3437c610..8f0846b60 100644 --- a/src/main/java/noppes/npcs/config/ConfigMain.java +++ b/src/main/java/noppes/npcs/config/ConfigMain.java @@ -80,7 +80,13 @@ public class ConfigMain public static Property DefaultMaxPartySizeProperty; public static int DefaultMaxPartySize = 4; - public static boolean EnableProfiles = true; + /** + * Profile Properties + **/ + public static boolean ProfilesEnabled = true; + public static boolean AllowProfileBackups = true; + public static int ProfileBackupAmount = 5; + /** * General NPC Properties @@ -220,7 +226,9 @@ public class ConfigMain TrackedQuestUpdateFrequency = TrackedQuestUpdateFrequencyProperty.getInt(5); // PROFILES - EnableProfiles = config.get(PROFILES, "Enable Profiles", true, "Allow the use of character Profiles").getBoolean(true); + ProfilesEnabled = config.get(PROFILES, "Enable Profiles", true, "Allow the use of character Profiles").getBoolean(true); + AllowProfileBackups = config.get(PROFILES, "Enable Profile Backups", true, "Will create backups of profile changes").getBoolean(true); + ProfileBackupAmount = config.get(PROFILES, "Number of Backups", 5, "How many backups per player to save").getInt(5); // Convert to Legacy if(CustomNpcs.legacyExist){