├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── other.md │ └── support---questions.md ├── .gitignore ├── LICENSE ├── META-INF └── MANIFEST.MF ├── OLD_LICENSE ├── README.md ├── icons ├── inventoryrollbackplus-icon.png ├── inventoryrollbackplus_icon_128.png ├── inventoryrollbackplus_icon_512.png └── inventoryrollbackplus_icon_69.png ├── pom.xml └── src ├── main ├── java │ ├── com │ │ └── nuclyon │ │ │ └── technicallycoded │ │ │ └── inventoryrollback │ │ │ ├── InventoryRollbackPlus.java │ │ │ ├── UpdateChecker.java │ │ │ ├── commands │ │ │ ├── Commands.java │ │ │ ├── IRPCommand.java │ │ │ └── inventoryrollback │ │ │ │ ├── DisableSubCmd.java │ │ │ │ ├── EnableSubCmd.java │ │ │ │ ├── ForceBackupSubCmd.java │ │ │ │ ├── HelpSubCmd.java │ │ │ │ ├── ImportSubCmd.java │ │ │ │ ├── ReloadSubCmd.java │ │ │ │ ├── RestoreSubCmd.java │ │ │ │ └── VersionSubCmd.java │ │ │ ├── customdata │ │ │ ├── CustomDataItemEditor.java │ │ │ └── ModernPdcItemEditor.java │ │ │ └── util │ │ │ ├── LegacyBackupConversionUtil.java │ │ │ ├── LogTimestamps.java │ │ │ ├── TimeZoneUtil.java │ │ │ ├── UserLogRateLimiter.java │ │ │ ├── serialization │ │ │ ├── DeserializationResult.java │ │ │ ├── ItemStackSerialization.java │ │ │ ├── Version1Serialization.java │ │ │ ├── Version2Serialization.java │ │ │ └── Version3Serialization.java │ │ │ └── test │ │ │ ├── SelfTest.java │ │ │ ├── SelfTestSerialization.java │ │ │ └── TestAssertions.java │ └── me │ │ └── danjono │ │ └── inventoryrollback │ │ ├── InventoryRollback.java │ │ ├── UpdateChecker.java │ │ ├── commands │ │ └── Commands.java │ │ ├── config │ │ ├── ConfigData.java │ │ ├── MessageData.java │ │ └── SoundData.java │ │ ├── data │ │ ├── LogType.java │ │ ├── MySQL.java │ │ ├── PlayerData.java │ │ └── YAML.java │ │ ├── gui │ │ ├── Buttons.java │ │ ├── InventoryName.java │ │ └── menu │ │ │ ├── EnderChestBackupMenu.java │ │ │ ├── MainInventoryBackupMenu.java │ │ │ ├── MainMenu.java │ │ │ ├── PlayerMenu.java │ │ │ └── RollbackListMenu.java │ │ ├── inventory │ │ ├── RestoreInventory.java │ │ └── SaveInventory.java │ │ ├── listeners │ │ ├── ClickGUI.java │ │ └── EventLogs.java │ │ └── reflections │ │ ├── LegacyNBTWrapper.java │ │ └── NMSHandler.java └── resources │ ├── .gitignore │ ├── LICENSE │ ├── config.yml │ ├── lang │ ├── en_us.yml │ ├── es_ar.yml │ ├── es_es.yml │ ├── fr_fr.yml │ ├── ru_ru.yml │ └── zh_cn.yml │ ├── messages.yml │ └── plugin.yml └── test └── java └── com └── nuclyon └── technicallycoded └── inventoryrollback └── util └── serialization ├── Version2SerializationIntTest.java └── Version2SerializationTest.java /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['paypal.me/technicallycoded'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Are your logs full of errors, something is broken, or not working as you think 4 | it should? 5 | title: 'Bug report: [short issue title]' 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | ### Server details: 12 | - Server version: *(REQUIRED) Type `/version` from your console and copy-paste the text. Approximate information will not be accepted* 13 | - IRP Version: *(REQUIRED) Type `/irp version` and copy-paste the information* 14 | 15 | ### Bug description: 16 | *REQUIRED: A clear and concise description of what the bug is and what you expected to happen.* 17 | *Example: "When the player moves around, the server TPS suffers. According to the spark profiler, IRP may be the cause."* 18 | 19 | ### How to reproduce: 20 | *REQUIRED: Steps to reproduce the issue.* 21 | *Example: 1.20.4 server with Vault & Luckperms. Join the server and type /irp bugged* 22 | 23 | ### Logs, screenshots & other: 24 | *You are REQUIRED to provide the full log file produced by the server when the bug occured (yes, even if it doesn't feel necessary). Issues opened without this information will be closed as invalid. More critical information is provided in the logs than you might realize.* 25 | 26 | *You are REQUIRED to use one of the following services for uploading your logs:* 27 | *`https://pastebin.com`, `https://mclo.gs`, `https://gist.github.com`, or simply upload the full file to this issue by dragging-and-dropping the file here* 28 | *Random website links will not be permitted or clicked by our team.* 29 | *For privacy minded people, `https://mclo.gs` will automatically remove IP addresses and operating system usernames.* 30 | 31 | *You may also include screenshots, logs, `/timings` links, `/spark profiler` links here.* 32 | 33 | #### **Additional info:** 34 | *Add any other context about the problem here. If not applicable just replace this line with N/A* 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Want something added to the plugin? 4 | title: 'Feature Request: (add a short feature title here)' 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What is missing: 11 | *Describe what you think is missing here.* 12 | 13 | ### Specific changes to make: 14 | *Describe how you think this should be added into future versions. (Any specific config changes or behaviors that should come along with this new feature?)* 15 | 16 | ### This feature already exists here: 17 | *If you are requesting a feature for this plugin that you have seen implemented in some other plugin or server, please link the plugin page or screenshots/a video of this feature being used. If not applicable just replace this line with N/A* 18 | 19 | #### **Additional context:** 20 | *Add any other context or info about this feature request here. If not applicable just replace this line with N/A* 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: Other type of issue or comment you want to provide 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support---questions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support & Questions 3 | about: If you have a question or know the plugin isn't broken but can't get something 4 | to work 5 | title: 'Support: [short support title]' 6 | labels: support 7 | assignees: '' 8 | 9 | --- 10 | 11 | *Ask your question here* 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .settings/ 3 | out/ 4 | target/ 5 | lib/ 6 | dependency-reduced-pom.xml 7 | InventoryRollback.iml 8 | InventoryRollbackPlus.iml 9 | .classpath 10 | .project 11 | /spigot_bb_code_description*.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 TechnicallyCoded 2 | 3 | All rights reserved. 4 | 5 | DISCLAIMER: THIS SOFTWARE IS PROVIDED "AS-IS" WITHOUT WARRANTY OF ANY KIND. 6 | UNDER NO CIRCUMSTANCE SHALL I OR OTHER CONTRIBUTING AUTHORS BE LIABLE FOR ANY 7 | DAMAGES OR OTHER LIABILITY. USE THIS SOFTWARE AT YOUR OWN RISK. 8 | I reserve the right to change this license at any time for any reason without 9 | notifying anyone or making anyone aware of such changes in any way. It is up 10 | to the user to make sure they stay up to date with the changes to this license. 11 | 12 | As per the MIT license requirements, you may find the license to 13 | the original code in the file named OLD_LICENSE. 14 | 15 | TechnicallyCoded refers to the person who's full legal name 16 | hashed using the bcrypt hashing function is: 17 | $2a$16$dlXkAIh2yC7ecbjohfw3C.CYwbNcdgIzyxG.LedJhdbbcCAZ.j2iy -------------------------------------------------------------------------------- /META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechnicallyCoded/Inventory-Rollback-Plus/f6eb1169e54d0a5390f27378111d2e9743a35471/META-INF/MANIFEST.MF -------------------------------------------------------------------------------- /OLD_LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 danjono 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/TechnicallyCoded/Inventory-Rollback/blob/master/icons/inventoryrollbackplus_icon_128.png?raw=true) 2 | # InventoryRollbackPlus 3 | 4 | ### Introduction 5 | 6 | **Description** 7 | 8 | InventoryRollbackPlus is a plugin which will backup player inventories for various events. This is very useful if players lose items due to lag, griefing and more! 9 | 10 | **When does the plugin backup player inventories?** 11 | 12 | When the a player: Joins, Leaves, Dies, Changes world, or when requested by staff. 13 | 14 | **What does the plugin save?** 15 | 16 | The plugin saves the player's: Inventory, Enderchest, Location, Health, Hunger, XP. 17 | 18 | *Note: This plugin is a fork (extended version) of InventoryRollback but with more features and faster updates.* 19 | 20 | **Why should you I use this version?** 21 | 22 | There are many core features missing from the original plugin. Here are some of the features in this version that are not present in the original: 23 | - Tab completion for commands 24 | - Single button click to restore the entire inventory 25 | - Help message if you run /inventoryrollback without anything else 26 | - & more coming soon.. 27 | 28 | **How do I use the plugin?** 29 | 30 | When a backup is created, it is added to a list of available backups to view and restore. 31 | 32 | Players with the required permission can open a rollback menu by running the command /ir restore . You will be presented will all the recent backups the plugin has made. To view a backup just click on the corresponding icon. You can now choose to restore what you want or go back to the list of backups. 33 | 34 | The plugin saves 50 deaths and 10 joins, leaves and world changes by deafult. New deaths, joins, leaves and world changes will push old backups into deleted space :O 35 | You can change these values in the configuration file. 36 | 37 | ### Documentation 38 | 39 | **Commands** 40 | 41 | - /ir restore - Open a menu to view all player backups 42 | - /ir forcebackup - Create a backup manually 43 | - /ir enable - Enable the plugin if disabled 44 | - /ir disable - Disable the plugin if enabled 45 | - /ir reload - Reload the configuration file 46 | 47 | **Permissions** 48 | 49 | - inventoryrollback.viewbackups - (Default: OP) Allow /ir restore command (without ability to give items back) 50 | - inventoryrollback.restore - (Default: OP) Allow /ir restore command 51 | - inventoryrollback.restore.teleport - (Default: OP) Allow player to teleport to location of backup 52 | - inventoryrollback.forcebackup - (Default: OP) Allow /ir forcebackup command 53 | - inventoryrollback.enable - (Default: OP) Allow /ir enable command 54 | - inventoryrollback.disable - (Default: OP) Allow /ir disable command 55 | - inventoryrollback.reload - (Default: OP) Allow /ir reload command 56 | - inventoryrollback.adminalerts - (Default: OP) Allow viewing important information for admins when they join 57 | 58 | - inventoryrollback.deathsave - (Default: All) Allow backup on death 59 | - inventoryrollback.joinsave - (Default: All) Allow backup on join 60 | - inventoryrollback.leavesave - (Default: All) Allow backup on leave 61 | - inventoryrollback.worldchangesave - (Default: All) Allow backup on world change 62 | - inventoryrollback.help - (Default: All) Allow viewing the help message of the plugin 63 | - inventoryrollback.version - (Default: All) Allow viewing version of the plugin 64 | 65 | ## Download Link 66 | [https://modrinth.com/plugin/inventoryrollbackplus](https://modrinth.com/plugin/inventoryrollbackplus) -------------------------------------------------------------------------------- /icons/inventoryrollbackplus-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechnicallyCoded/Inventory-Rollback-Plus/f6eb1169e54d0a5390f27378111d2e9743a35471/icons/inventoryrollbackplus-icon.png -------------------------------------------------------------------------------- /icons/inventoryrollbackplus_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechnicallyCoded/Inventory-Rollback-Plus/f6eb1169e54d0a5390f27378111d2e9743a35471/icons/inventoryrollbackplus_icon_128.png -------------------------------------------------------------------------------- /icons/inventoryrollbackplus_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechnicallyCoded/Inventory-Rollback-Plus/f6eb1169e54d0a5390f27378111d2e9743a35471/icons/inventoryrollbackplus_icon_512.png -------------------------------------------------------------------------------- /icons/inventoryrollbackplus_icon_69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechnicallyCoded/Inventory-Rollback-Plus/f6eb1169e54d0a5390f27378111d2e9743a35471/icons/inventoryrollbackplus_icon_69.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | com.nuclyon.technicallycoded.inventoryrollback 7 | InventoryRollbackPlus 8 | 1.7.4 9 | jar 10 | InventoryRollbackPlus 11 | https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/ 12 | 13 | 14 | TechnicallyCoded 15 | https://github.com/TechnicallyCoded/ 16 | 17 | 18 | 19 | UTF-8 20 | 21 | 22 | 23 | 24 | spigot-repo 25 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 26 | 27 | 28 | CodeMC 29 | https://repo.codemc.org/repository/maven-public 30 | 31 | 32 | papermc 33 | https://repo.papermc.io/repository/maven-public/ 34 | 35 | 36 | maven_central 37 | Maven Central 38 | https://repo.maven.apache.org/maven2/ 39 | 40 | 41 | jitpack 42 | https://jitpack.io 43 | 44 | 45 | tcoded-releases 46 | https://repo.tcoded.com/releases/ 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.spigotmc 54 | spigot-api 55 | 1.20.5-R0.1-SNAPSHOT 56 | provided 57 | 58 | 67 | 68 | org.spigotmc 69 | spigot 70 | 1.20.5-R0.1-SNAPSHOT 71 | true 72 | provided 73 | 74 | 75 | 76 | org.bstats 77 | bstats-bukkit 78 | 3.0.2 79 | compile 80 | 81 | 82 | 83 | io.papermc 84 | paperlib 85 | 1.0.6 86 | compile 87 | 88 | 89 | org.jetbrains 90 | annotations 91 | 24.1.0 92 | compile 93 | 94 | 95 | 96 | org.apache.commons 97 | commons-lang3 98 | 3.14.0 99 | compile 100 | 101 | 102 | com.tcoded.lightlibs 103 | BukkitVersion 104 | 0.0.12 105 | compile 106 | 107 | 108 | org.junit.jupiter 109 | junit-jupiter 110 | 5.9.3 111 | test 112 | 113 | 114 | 115 | 116 | ${project.name}-${project.version} 117 | 118 | 119 | ${project.basedir}/src/main/resources 120 | 121 | .gitignore 122 | 123 | true 124 | 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-compiler-plugin 130 | 3.8.1 131 | 132 | 8 133 | 8 134 | true 135 | true 136 | 137 | 138 | 139 | org.apache.maven.plugins 140 | maven-shade-plugin 141 | 3.5.3 142 | 143 | ${project.build.directory}/dependency-reduced-pom.xml 144 | 145 | 146 | io.papermc:paperlib 147 | 148 | io/papermc/lib/** 149 | 150 | 151 | **/MANIFEST.MF 152 | 153 | 154 | 155 | org.bstats:bstats-bukkit 156 | 157 | org/bstats/** 158 | 159 | 160 | **/MANIFEST.MF 161 | 162 | 163 | 164 | *:* 165 | 166 | META-INF/maven/** 167 | 168 | 169 | 170 | 171 | 172 | org.bstats 173 | ${project.groupId}.bstats 174 | 175 | 176 | io.papermc.lib 177 | ${project.groupId}.paperlib 178 | 179 | 180 | com.tcoded.lightlibs.bukkitversion 181 | ${project.groupId}.bukkitversion 182 | 183 | 184 | 185 | 186 | 187 | package 188 | 189 | shade 190 | 191 | 192 | 193 | 194 | 195 | org.apache.maven.plugins 196 | maven-jar-plugin 197 | 198 | 199 | 200 | TechnicallyCoded 201 | ${project.url} 202 | 203 | ${project.basedir}/META-INF/MANIFEST.MF 204 | 205 | 206 | 207 | 208 | 209 | Allows server moderators to restore player items and data from backups. 210 | 211 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/UpdateChecker.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback; 2 | 3 | import org.bukkit.plugin.java.JavaPlugin; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.net.MalformedURLException; 9 | import java.net.URL; 10 | import java.net.URLConnection; 11 | 12 | public class UpdateChecker { 13 | 14 | private final JavaPlugin plugin; 15 | private URL checkURL; 16 | 17 | private final String currentVersion; 18 | private final String[] currVersionSections; 19 | private String availableVersion; 20 | 21 | private final UpdateResult result; 22 | 23 | public static class UpdateResult { 24 | 25 | private Type type; 26 | private String latestVer; 27 | private String currentVer; 28 | 29 | public UpdateResult(Type typeIn, String latestVerIn, String currentVerIn) { 30 | this.type = typeIn; 31 | this.latestVer = latestVerIn; 32 | this.currentVer = currentVerIn; 33 | } 34 | 35 | public void setType(Type typeIn) { this.type = typeIn; } 36 | public void setLatestVer(String latestVerIn) { this.latestVer = latestVerIn; } 37 | 38 | public Type getType() { return this.type; } 39 | public String getCurrentVer() { return this.currentVer; } 40 | public String getLatestVer() { return this.latestVer; } 41 | 42 | public enum Type { 43 | NO_UPDATE, 44 | FAIL_SPIGOT, 45 | UNKNOWN_VERSION, 46 | UPDATE_LOW, 47 | UPDATE_MEDIUM, 48 | UPDATE_HIGH, 49 | DEV_BUILD 50 | } 51 | } 52 | 53 | public UpdateChecker(JavaPlugin plugin, Integer resourceId) { 54 | this.plugin = plugin; 55 | this.currentVersion = this.plugin.getDescription().getVersion(); 56 | this.currVersionSections = currentVersion.split("\\."); 57 | 58 | this.result = new UpdateResult(UpdateResult.Type.FAIL_SPIGOT, null, this.currentVersion); 59 | 60 | try { 61 | this.checkURL = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + resourceId); 62 | } catch (MalformedURLException e) { 63 | result.setType(UpdateResult.Type.FAIL_SPIGOT); 64 | return; 65 | } 66 | 67 | run(); 68 | } 69 | 70 | private void run() { 71 | URLConnection con; 72 | try { 73 | con = checkURL.openConnection(); 74 | } catch (IOException e1) { 75 | result.setType(UpdateResult.Type.FAIL_SPIGOT); 76 | return; 77 | } 78 | 79 | try { 80 | availableVersion = new BufferedReader(new InputStreamReader(con.getInputStream())).readLine(); 81 | } catch (IOException e) { 82 | result.setType(UpdateResult.Type.FAIL_SPIGOT); 83 | return; 84 | } 85 | 86 | if (availableVersion.isEmpty()) { 87 | result.setType(UpdateResult.Type.FAIL_SPIGOT); 88 | return; 89 | } 90 | 91 | result.setLatestVer(availableVersion); 92 | 93 | // Version sections of remote 94 | String[] versionSections = availableVersion.split("\\."); 95 | // Test diff 96 | for (int i = 0; i < versionSections.length || i < currVersionSections.length; i++) { // Continue until both versions run out of sub sections 97 | try { 98 | // - Detailed walk through - 99 | // Statement below means: if number of sections is i+1 or greater 100 | // (Example: 5.3.2 has 3 sections and i = 0, true) 101 | // - Explanation - 102 | // 5.3.2 has indexes 0-2, which also means if i = 3, we are on the 4th iteration and 103 | // ran out of available sub-sections 104 | boolean vSecExists = versionSections.length - i > 0; 105 | boolean cvSecExists = currVersionSections.length - i > 0; // current version has that many sections too? 106 | if (!vSecExists) { // if remote version doesn't have that many sub sections (aka, we are running something newer) 107 | result.setType(UpdateResult.Type.DEV_BUILD); 108 | return; 109 | } else if (!cvSecExists) { // if local version doesn't have that many sub sections (aka remote is running something newer) 110 | result.setType(getUpdateResultPriority(i)); 111 | return; 112 | } 113 | int vSecInt = Integer.parseInt(versionSections[i]); // get int value of remote sub-section value 114 | int cvSecInt = Integer.parseInt(currVersionSections[i]); // get int value of local sub-section value 115 | if (vSecInt > cvSecInt) { // remote > local ? We are out of date. 116 | result.setType(getUpdateResultPriority(i)); 117 | return; 118 | } else if (cvSecInt > vSecInt) { // local > remote ? We are running something not yet released! 119 | result.setType(UpdateResult.Type.DEV_BUILD); 120 | return; 121 | } 122 | } catch (NumberFormatException e) { 123 | result.setType(UpdateResult.Type.UNKNOWN_VERSION); // Not a parsable number? Unknown version, since we can't compare! 124 | return; 125 | } 126 | } 127 | result.setType(UpdateResult.Type.NO_UPDATE); 128 | } 129 | 130 | public UpdateResult getResult() { 131 | return this.result; 132 | } 133 | 134 | public String getVersion() { 135 | return this.availableVersion; 136 | } 137 | 138 | public UpdateResult.Type getUpdateResultPriority(int i) { 139 | switch (i) { 140 | case 0: 141 | return UpdateResult.Type.UPDATE_HIGH; 142 | case 1: 143 | return UpdateResult.Type.UPDATE_MEDIUM; 144 | default: 145 | return UpdateResult.Type.UPDATE_LOW; 146 | } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/Commands.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback.*; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | 10 | import org.bukkit.command.Command; 11 | import org.bukkit.command.CommandExecutor; 12 | import org.bukkit.command.CommandSender; 13 | import org.bukkit.command.TabCompleter; 14 | 15 | public class Commands implements CommandExecutor, TabCompleter { 16 | 17 | private InventoryRollbackPlus main; 18 | 19 | private String[] defaultOptions = new String[] {"restore", "forcebackup", "enable", "disable", "reload", "version", "import", "help"}; 20 | private String[] backupOptions = new String[] {"all", "player"}; 21 | private String[] importOptions = new String[] {"confirm"}; 22 | 23 | private HashMap subCommands = new HashMap<>(); 24 | 25 | public Commands(InventoryRollbackPlus mainIn) { 26 | this.main = mainIn; 27 | this.subCommands.put("restore", new RestoreSubCmd(mainIn)); 28 | this.subCommands.put("enable", new EnableSubCmd(mainIn)); 29 | this.subCommands.put("disable", new DisableSubCmd(mainIn)); 30 | this.subCommands.put("reload", new ReloadSubCmd(mainIn)); 31 | this.subCommands.put("version", new VersionSubCmd(mainIn)); 32 | this.subCommands.put("forcebackup", new ForceBackupSubCmd(mainIn)); 33 | this.subCommands.put("import", new ImportSubCmd(mainIn)); 34 | this.subCommands.put("help", new HelpSubCmd(mainIn)); 35 | } 36 | 37 | public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { 38 | if (label.equalsIgnoreCase("inventoryrollback") || 39 | label.equalsIgnoreCase("ir") || 40 | label.equalsIgnoreCase("irp") || 41 | label.equalsIgnoreCase("inventoryrollbackplus") 42 | ) { 43 | if (args.length == 0) { 44 | ((HelpSubCmd) this.subCommands.get("help")).sendHelp(sender); 45 | return true; 46 | } 47 | IRPCommand irpCmd = this.subCommands.get(args[0]); 48 | if (irpCmd != null) { 49 | irpCmd.onCommand(sender, cmd, label, args); 50 | return true; 51 | } 52 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getError()); 53 | } 54 | return true; 55 | } 56 | 57 | public List onTabComplete(CommandSender commandSender, Command command, String name, String[] args) { 58 | if (args.length == 1) { 59 | ArrayList suggestions = new ArrayList<>(); 60 | for (String option : this.defaultOptions) { 61 | if (option.startsWith(args[0].toLowerCase()) && commandSender.hasPermission("inventoryrollbackplus." + option)) 62 | suggestions.add(option); 63 | } 64 | return suggestions; 65 | } else if (args.length == 2) { 66 | String[] opts; 67 | 68 | if ((args[0].equalsIgnoreCase("forcebackup") || 69 | args[0].equalsIgnoreCase("forcesave")) && 70 | commandSender.hasPermission("inventoryrollbackplus.forcebackup") 71 | ) { 72 | opts = this.backupOptions; 73 | 74 | } else if (args[0].equalsIgnoreCase("import") && 75 | (ImportSubCmd.shouldShowConfirmOption() || args[1].toLowerCase().startsWith("c")) && 76 | commandSender.hasPermission("inventoryrollbackplus.import") 77 | ) { 78 | opts = this.importOptions; 79 | 80 | } else { 81 | opts = null; 82 | } 83 | 84 | if (opts == null) return null; 85 | 86 | ArrayList suggestions = new ArrayList<>(); 87 | for (String option : opts) { 88 | if (option.startsWith(args[1].toLowerCase())) 89 | suggestions.add(option); 90 | } 91 | return suggestions; 92 | } 93 | return null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/IRPCommand.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import org.bukkit.command.Command; 5 | import org.bukkit.command.CommandSender; 6 | 7 | public abstract class IRPCommand { 8 | 9 | public InventoryRollbackPlus main; 10 | 11 | public IRPCommand(InventoryRollbackPlus mainIn) { 12 | this.main = mainIn; 13 | } 14 | 15 | public abstract void onCommand(CommandSender sender, Command cmd, String label, String[] args); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/DisableSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import me.danjono.inventoryrollback.config.ConfigData; 6 | import me.danjono.inventoryrollback.config.MessageData; 7 | import org.bukkit.command.Command; 8 | import org.bukkit.command.CommandSender; 9 | 10 | public class DisableSubCmd extends IRPCommand { 11 | 12 | public DisableSubCmd(InventoryRollbackPlus mainIn) { 13 | super(mainIn); 14 | } 15 | 16 | @Override 17 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 18 | if (sender.hasPermission("inventoryrollbackplus.disable")) { 19 | ConfigData.setEnabled(false); 20 | main.getConfigData().saveConfig(); 21 | 22 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getPluginDisabled()); 23 | } else { 24 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoPermission()); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/EnableSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import org.bukkit.command.Command; 7 | import org.bukkit.command.CommandSender; 8 | 9 | public class EnableSubCmd extends IRPCommand { 10 | 11 | public EnableSubCmd(InventoryRollbackPlus mainIn) { 12 | super(mainIn); 13 | } 14 | 15 | @Override 16 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 17 | if (sender.hasPermission("inventoryrollbackplus.enable")) { 18 | main.getConfigData().setEnabled(true); 19 | main.getConfigData().saveConfig(); 20 | 21 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getPluginEnabled()); 22 | } else { 23 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoPermission()); 24 | } 25 | return; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/ForceBackupSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import me.danjono.inventoryrollback.data.LogType; 7 | import me.danjono.inventoryrollback.inventory.SaveInventory; 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.OfflinePlayer; 10 | import org.bukkit.command.Command; 11 | import org.bukkit.command.CommandSender; 12 | import org.bukkit.entity.Player; 13 | 14 | public class ForceBackupSubCmd extends IRPCommand { 15 | 16 | public ForceBackupSubCmd(InventoryRollbackPlus mainIn) { 17 | super(mainIn); 18 | } 19 | 20 | @Override 21 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 22 | if (sender.hasPermission("inventoryrollbackplus.forcebackup")) { 23 | if (args.length == 1 || args.length > 3) { 24 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getError()); 25 | return; 26 | } 27 | 28 | if (args[1].equalsIgnoreCase("all")) { 29 | forceBackupAll(sender); 30 | } else if (args[1].equalsIgnoreCase("player")) { 31 | forceBackupPlayer(sender, args); 32 | } else { 33 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getError()); 34 | } 35 | } else { 36 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoPermission()); 37 | } 38 | } 39 | 40 | private void forceBackupAll(CommandSender sender) { 41 | for (Player player : Bukkit.getOnlinePlayers()) { 42 | new SaveInventory(player, LogType.FORCE, null, null) 43 | .snapshotAndSave(player.getInventory(), player.getEnderChest(), true); 44 | } 45 | 46 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getForceBackupAll()); 47 | } 48 | 49 | private void forceBackupPlayer(CommandSender sender, String[] args) { 50 | if (args.length == 2) { 51 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getError()); 52 | return; 53 | } 54 | 55 | OfflinePlayer offlinePlayer = Bukkit.getPlayer(args[2]); 56 | 57 | if (offlinePlayer == null) { 58 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNotOnlineError(args[2])); 59 | return; 60 | } 61 | 62 | if (!offlinePlayer.isOnline()) { 63 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNotOnlineError(offlinePlayer.getName())); 64 | return; 65 | } 66 | 67 | Player player = (Player) offlinePlayer; 68 | new SaveInventory(player, LogType.FORCE, null, null) 69 | .snapshotAndSave(player.getInventory(), player.getEnderChest(), true); 70 | 71 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getForceBackupPlayer(offlinePlayer.getName())); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/HelpSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import org.bukkit.ChatColor; 7 | import org.bukkit.command.Command; 8 | import org.bukkit.command.CommandSender; 9 | 10 | public class HelpSubCmd extends IRPCommand { 11 | 12 | public HelpSubCmd(InventoryRollbackPlus mainIn) { 13 | super(mainIn); 14 | } 15 | 16 | @Override 17 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 18 | if (sender.hasPermission("inventoryrollbackplus.help")) { 19 | this.sendHelp(sender); 20 | } else { 21 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoPermission()); 22 | } 23 | return; 24 | } 25 | 26 | public void sendHelp(CommandSender sender) { 27 | sender.sendMessage( 28 | MessageData.getPluginPrefix() + ChatColor.GRAY + "InventoryRollbackPlus - by TechnicallyCoded\n" + 29 | ChatColor.WHITE + " Available Commands:\n" + 30 | ChatColor.WHITE + " /irp restore [player]" + ChatColor.GRAY + " - Open rollback GUI for optional [player]\n" + 31 | ChatColor.WHITE + " /irp forcebackup [player]" + ChatColor.GRAY + " - Create a forced save of a player's inventory\n" + 32 | ChatColor.WHITE + " /irp enable" + ChatColor.GRAY + " - Enable the plugin\n" + 33 | ChatColor.WHITE + " /irp disable" + ChatColor.GRAY + " - Disable the plugin\n" + 34 | ChatColor.WHITE + " /irp reload" + ChatColor.GRAY + " - Reload the plugin\n" + 35 | ChatColor.WHITE + " /irp help" + ChatColor.GRAY + " - Get this message\n" + 36 | ChatColor.WHITE + " /irp version" + ChatColor.GRAY + " - Get plugin info & version\n"); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/ImportSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import com.nuclyon.technicallycoded.inventoryrollback.util.LegacyBackupConversionUtil; 6 | import me.danjono.inventoryrollback.config.MessageData; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.command.Command; 10 | import org.bukkit.command.CommandSender; 11 | 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | 14 | public class ImportSubCmd extends IRPCommand { 15 | 16 | private static final AtomicBoolean suggestConfirm = new AtomicBoolean(false); 17 | 18 | public ImportSubCmd(InventoryRollbackPlus mainIn) { 19 | super(mainIn); 20 | } 21 | 22 | @Override 23 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 24 | if (sender.hasPermission("inventoryrollbackplus.import")) { 25 | 26 | // Check that player confirms this operation 27 | if (args.length < 2 || !args[1].equalsIgnoreCase("confirm")) { 28 | // Send player help 29 | sender.sendMessage(ChatColor.RED + "/" + label.toLowerCase() + " import " + ChatColor.BOLD + "confirm"); 30 | 31 | // Handle suggestions 32 | suggestConfirm.set(true); 33 | 34 | // Reset suggestion availability after 10 seconds 35 | this.main.getServer().getScheduler().runTaskLaterAsynchronously(this.main, () -> { 36 | suggestConfirm.set(false); 37 | }, 10 * 20); 38 | 39 | return; 40 | } 41 | 42 | // Execute import 43 | Bukkit.getScheduler().runTaskAsynchronously(main, LegacyBackupConversionUtil::convertOldBackupData); 44 | 45 | // Reset suggestion to not visible 46 | suggestConfirm.set(false); 47 | 48 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getImportSuccess()); 49 | } else { 50 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoPermission()); 51 | } 52 | return; 53 | } 54 | 55 | public static boolean shouldShowConfirmOption() { 56 | return suggestConfirm.get(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/ReloadSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import me.danjono.inventoryrollback.config.ConfigData; 6 | import me.danjono.inventoryrollback.config.MessageData; 7 | import org.bukkit.command.Command; 8 | import org.bukkit.command.CommandSender; 9 | 10 | public class ReloadSubCmd extends IRPCommand { 11 | 12 | public ReloadSubCmd(InventoryRollbackPlus mainIn) { 13 | super(mainIn); 14 | } 15 | 16 | @Override 17 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 18 | if (sender.hasPermission("inventoryrollbackplus.reload")) { 19 | ConfigData config = main.getConfigData(); 20 | config.generateConfigFile(); 21 | config.setVariables(); 22 | main.startupTasks(); 23 | 24 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getPluginReload()); 25 | } else { 26 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoPermission()); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/RestoreSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import me.danjono.inventoryrollback.InventoryRollback; 6 | import me.danjono.inventoryrollback.config.ConfigData; 7 | import me.danjono.inventoryrollback.config.MessageData; 8 | import me.danjono.inventoryrollback.gui.menu.MainMenu; 9 | import me.danjono.inventoryrollback.gui.menu.PlayerMenu; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.OfflinePlayer; 12 | import org.bukkit.command.Command; 13 | import org.bukkit.command.CommandSender; 14 | import org.bukkit.entity.Player; 15 | 16 | import java.util.UUID; 17 | 18 | public class RestoreSubCmd extends IRPCommand { 19 | 20 | public RestoreSubCmd(InventoryRollbackPlus mainIn) { 21 | super(mainIn); 22 | } 23 | 24 | @Override 25 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 26 | if (sender instanceof Player) { 27 | if (sender.hasPermission("inventoryrollbackplus.viewbackups")) { 28 | if (!ConfigData.isEnabled()) { 29 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getPluginDisabled()); 30 | return; 31 | } 32 | Player staff = (Player) sender; 33 | openBackupMenu(sender, staff, args); 34 | } else { 35 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoPermission()); 36 | } 37 | } else { 38 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getPlayerOnlyError()); 39 | } 40 | } 41 | 42 | @SuppressWarnings("deprecation") 43 | private void openBackupMenu(CommandSender sender, Player staff, String[] args) { 44 | if (args.length <= 0 || args.length == 1) { 45 | try { 46 | openMainMenu(staff); 47 | } catch (NullPointerException ignored) {} 48 | } else if(args.length == 2) { 49 | OfflinePlayer rollbackPlayer; 50 | 51 | String uuidStr = args[1]; 52 | 53 | // Handle input of UUID 54 | if (uuidStr.length() == 36 || args[1].length() == 32) { 55 | 56 | // Handle malformed UUID 57 | if (args[1].length() == 32) { 58 | String oldUuidStr = uuidStr; 59 | uuidStr = oldUuidStr.substring(0, 8); 60 | uuidStr += "-"; 61 | uuidStr += oldUuidStr.substring(8, 12); 62 | uuidStr += "-"; 63 | uuidStr += oldUuidStr.substring(12, 16); 64 | uuidStr += "-"; 65 | uuidStr += oldUuidStr.substring(16, 20); 66 | uuidStr += "-"; 67 | uuidStr += oldUuidStr.substring(20); 68 | } 69 | 70 | try { 71 | rollbackPlayer = Bukkit.getOfflinePlayer(UUID.fromString(uuidStr)); 72 | } catch (IllegalArgumentException e) { 73 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getError()); 74 | return; 75 | } 76 | } else { 77 | // If not UUID length, assume it's a name 78 | rollbackPlayer = Bukkit.getOfflinePlayer(args[1]); 79 | } 80 | 81 | try { 82 | openPlayerMenu(staff, rollbackPlayer); 83 | } catch (NullPointerException e) {} 84 | } else { 85 | sender.sendMessage(MessageData.getPluginPrefix() + MessageData.getError()); 86 | } 87 | } 88 | 89 | private void openMainMenu(Player staff) { 90 | MainMenu menu = new MainMenu(staff, 1); 91 | 92 | staff.openInventory(menu.getInventory()); 93 | Bukkit.getScheduler().runTaskAsynchronously(InventoryRollback.getInstance(), menu::getMainMenu); 94 | } 95 | 96 | private void openPlayerMenu(Player staff, OfflinePlayer offlinePlayer) { 97 | PlayerMenu menu = new PlayerMenu(staff, offlinePlayer); 98 | 99 | staff.openInventory(menu.getInventory()); 100 | Bukkit.getScheduler().runTaskAsynchronously(InventoryRollback.getInstance(), menu::getPlayerMenu); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/commands/inventoryrollback/VersionSubCmd.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.commands.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.commands.IRPCommand; 5 | import me.danjono.inventoryrollback.InventoryRollback; 6 | import me.danjono.inventoryrollback.config.MessageData; 7 | import org.bukkit.ChatColor; 8 | import org.bukkit.command.Command; 9 | import org.bukkit.command.CommandSender; 10 | 11 | public class VersionSubCmd extends IRPCommand { 12 | 13 | public VersionSubCmd(InventoryRollbackPlus mainIn) { 14 | super(mainIn); 15 | } 16 | 17 | @Override 18 | public void onCommand(CommandSender sender, Command cmd, String label, String[] args) { 19 | StringBuilder strb = new StringBuilder(MessageData.getPluginPrefix()); 20 | boolean hasVersionPerm = sender.hasPermission("inventoryrollbackplus.version"); 21 | 22 | strb.append("\n") 23 | .append(ChatColor.WHITE) 24 | .append("Plugin:").append("\n") 25 | .append(ChatColor.GRAY) 26 | .append(" Running InventoryRollbackPlus"); 27 | // Can see version? 28 | if (hasVersionPerm) strb.append(" v").append(InventoryRollback.getPluginVersion()); 29 | strb.append("\n"); 30 | // Else show warning 31 | if (!hasVersionPerm) 32 | strb.append(ChatColor.GRAY) 33 | .append(" (Version not visible, lacking permission)") 34 | .append("\n"); 35 | 36 | strb.append(ChatColor.WHITE) 37 | .append("Authors:").append("\n") 38 | .append(ChatColor.GRAY) 39 | .append(" - Maintained/updated by: TechnicallyCoded").append("\n") 40 | .append(" - Original author: danjono").append("\n") 41 | .append("\n") 42 | .append(ChatColor.WHITE).append("Update link:").append("\n") 43 | .append(ChatColor.BLUE).append(ChatColor.ITALIC).append(" https://www.spigotmc.org/resources/inventoryrollback-plus.85811/"); 44 | 45 | 46 | // Send 47 | sender.sendMessage(strb.toString()); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/customdata/CustomDataItemEditor.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.customdata; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.tcoded.lightlibs.bukkitversion.MCVersion; 5 | import me.danjono.inventoryrollback.reflections.LegacyNBTWrapper; 6 | import org.bukkit.inventory.ItemStack; 7 | 8 | public interface CustomDataItemEditor { 9 | 10 | static CustomDataItemEditor editItem(ItemStack item) { 11 | if (InventoryRollbackPlus.getInstance().getVersion().greaterOrEqThan(MCVersion.v1_20_4.toBukkitVersion())) { 12 | return new ModernPdcItemEditor(item); 13 | } else { 14 | return new LegacyNBTWrapper(item); 15 | } 16 | } 17 | 18 | boolean hasUUID(); 19 | 20 | ItemStack setString(String key, String data); 21 | 22 | ItemStack setInt(String key, Integer data); 23 | 24 | ItemStack setLong(String key, Long data); 25 | 26 | ItemStack setDouble(String key, Double data); 27 | 28 | ItemStack setFloat(String key, Float data); 29 | 30 | String getString(String key); 31 | 32 | int getInt(String key); 33 | 34 | Long getLong(String key); 35 | 36 | double getDouble(String key); 37 | 38 | Float getFloat(String key); 39 | 40 | ItemStack setItemData(); 41 | 42 | } -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/customdata/ModernPdcItemEditor.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.customdata; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import org.bukkit.NamespacedKey; 5 | import org.bukkit.inventory.ItemStack; 6 | import org.bukkit.inventory.meta.ItemMeta; 7 | import org.bukkit.persistence.PersistentDataContainer; 8 | import org.bukkit.persistence.PersistentDataType; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public class ModernPdcItemEditor implements CustomDataItemEditor { 12 | 13 | private final ItemStack item; 14 | 15 | public ModernPdcItemEditor(ItemStack item) { 16 | this.item = item; 17 | } 18 | 19 | @Override 20 | public boolean hasUUID() { 21 | String uuid = getString("uuid"); 22 | return (uuid != null && !uuid.isEmpty()); 23 | } 24 | 25 | @Override 26 | public ItemStack setString(String key, String data) { 27 | setData(key, PersistentDataType.STRING, data); 28 | return item; 29 | } 30 | 31 | @Override 32 | public ItemStack setInt(String key, Integer data) { 33 | setData(key, PersistentDataType.INTEGER, data); 34 | return item; 35 | } 36 | 37 | @Override 38 | public ItemStack setLong(String key, Long data) { 39 | setData(key, PersistentDataType.LONG, data); 40 | return item; 41 | } 42 | 43 | @Override 44 | public ItemStack setDouble(String key, Double data) { 45 | setData(key, PersistentDataType.DOUBLE, data); 46 | return item; 47 | } 48 | 49 | @Override 50 | public ItemStack setFloat(String key, Float data) { 51 | setData(key, PersistentDataType.FLOAT, data); 52 | return item; 53 | } 54 | 55 | @Override 56 | public String getString(String key) { 57 | return getData(key, PersistentDataType.STRING); 58 | } 59 | 60 | @Override 61 | public int getInt(String key) { 62 | return getData(key, PersistentDataType.INTEGER); 63 | } 64 | 65 | @Override 66 | public Long getLong(String key) { 67 | return getData(key, PersistentDataType.LONG); 68 | } 69 | 70 | @Override 71 | public double getDouble(String key) { 72 | return getData(key, PersistentDataType.DOUBLE); 73 | } 74 | 75 | @Override 76 | public Float getFloat(String key) { 77 | return getData(key, PersistentDataType.FLOAT); 78 | } 79 | 80 | @Override 81 | public ItemStack setItemData() { 82 | return item; 83 | } 84 | 85 | private T getData(String key, PersistentDataType type) { 86 | if (item == null) return null; 87 | 88 | ItemMeta itemMeta = item.getItemMeta(); 89 | PersistentDataContainer pdc = itemMeta.getPersistentDataContainer(); 90 | 91 | NamespacedKey namedKey = getNamespacedKey(key); 92 | 93 | if (!pdc.has(namedKey, type)) return null; 94 | return pdc.get(namedKey, type); 95 | } 96 | 97 | private void setData(String key, PersistentDataType type, T data) { 98 | if (item == null) return; 99 | 100 | ItemMeta itemMeta = item.getItemMeta(); 101 | PersistentDataContainer pdc = itemMeta.getPersistentDataContainer(); 102 | pdc.set(getNamespacedKey(key), type, data); 103 | 104 | item.setItemMeta(itemMeta); 105 | } 106 | 107 | private static @NotNull NamespacedKey getNamespacedKey(String key) { 108 | return new NamespacedKey(InventoryRollbackPlus.getInstance(), key); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/LegacyBackupConversionUtil.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import me.danjono.inventoryrollback.config.ConfigData; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import me.danjono.inventoryrollback.data.LogType; 7 | import me.danjono.inventoryrollback.data.PlayerData; 8 | import me.danjono.inventoryrollback.inventory.RestoreInventory; 9 | import org.apache.commons.lang3.Validate; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.configuration.ConfigurationSection; 12 | import org.bukkit.configuration.InvalidConfigurationException; 13 | import org.bukkit.configuration.file.YamlConfiguration; 14 | import org.bukkit.inventory.ItemStack; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.io.File; 18 | import java.io.FileNotFoundException; 19 | import java.io.IOException; 20 | import java.util.*; 21 | import java.util.logging.Logger; 22 | 23 | public class LegacyBackupConversionUtil { 24 | 25 | public static Map oldLogTypesMap = new HashMap<>(); 26 | 27 | static { 28 | oldLogTypesMap.put("WORLDCHANGE", LogType.WORLD_CHANGE); 29 | } 30 | 31 | public static void convertOldBackupData() { 32 | Logger logger = InventoryRollbackPlus.getPluginLogger(); 33 | 34 | List oldBackupTypeFolders = new ArrayList<>(); 35 | 36 | File oldBackupsRoot = new File(ConfigData.getFolderLocation().getParentFile(), "InventoryRollback/saves"); 37 | 38 | oldBackupTypeFolders.add(new File(oldBackupsRoot, "deaths")); 39 | oldBackupTypeFolders.add(new File(oldBackupsRoot, "joins")); 40 | oldBackupTypeFolders.add(new File(oldBackupsRoot, "quits")); 41 | oldBackupTypeFolders.add(new File(oldBackupsRoot, "worldChanges")); 42 | oldBackupTypeFolders.add(new File(oldBackupsRoot, "force")); 43 | 44 | int logTypeNumber = -1; 45 | List logTypes = new ArrayList<>(); 46 | logTypes.add(LogType.DEATH); 47 | logTypes.add(LogType.JOIN); 48 | logTypes.add(LogType.QUIT); 49 | logTypes.add(LogType.WORLD_CHANGE); 50 | logTypes.add(LogType.FORCE); 51 | 52 | for (File oldBackupFolder : oldBackupTypeFolders) { 53 | 54 | logTypeNumber++; 55 | 56 | if (!oldBackupFolder.exists()) { 57 | logger.warning(MessageData.getPluginPrefix() + "Backup folder does not exist at " + oldBackupFolder.getAbsolutePath() + "! Skipping..."); 58 | continue; 59 | } 60 | 61 | // Add all YAML files to list 62 | File[] availableFiles = oldBackupFolder.listFiles(); 63 | if (availableFiles == null) continue; 64 | 65 | List backupFilesToConvert = getFilesToConvert(availableFiles); 66 | 67 | LogType currLogTypeProcessing = logTypes.get(logTypeNumber); 68 | logger.info(MessageData.getPluginPrefix() + "Converting the backup location " + currLogTypeProcessing.name()); 69 | 70 | for (File backupFile : backupFilesToConvert) { 71 | convertBackupFile(logger, backupFile, currLogTypeProcessing); 72 | } 73 | 74 | } 75 | 76 | InventoryRollbackPlus.getPluginLogger().info(MessageData.getPluginPrefix() + "Conversion completed!"); 77 | } 78 | 79 | private static ArrayList getFilesToConvert(File[] availableFiles) { 80 | ArrayList backupFiles = new ArrayList<>(); 81 | 82 | // Sanity check files in the folder & add them to a list to process later 83 | for (File file : availableFiles) { 84 | String originalFileName = file.getName(); 85 | String[] fileParts = originalFileName.split("\\."); 86 | String fileUUIDStr = fileParts[0]; 87 | String fileExtension = fileParts[1]; 88 | 89 | UUID playerUuid; 90 | try { 91 | // Check if it's a valid UUID 92 | playerUuid = UUID.fromString(fileUUIDStr); 93 | } catch (IllegalArgumentException ex) { 94 | InventoryRollbackPlus.getInstance().getLogger().severe( 95 | "An error occurred when trying to retrieve a old backup player UUID! " + 96 | "Please seek help in the issues section of the InventryRollbackPlus github page."); 97 | ex.printStackTrace(); 98 | continue; 99 | } 100 | 101 | if (file.isFile() && fileExtension.equals("yml")) { 102 | backupFiles.add(file); 103 | } 104 | } 105 | 106 | return backupFiles; 107 | } 108 | 109 | public static void convertBackupFile(Logger logger, File backupFile, LogType logTypeProcessing) { 110 | YamlConfiguration oldBackupDataConfig; 111 | oldBackupDataConfig = loadConfiguration(backupFile); 112 | 113 | if (oldBackupDataConfig == null) { 114 | logger.warning(MessageData.getPluginPrefix() + "Error converting backup file at " + 115 | backupFile.getAbsolutePath() + " - Invalid YAML format possibly from corruption."); 116 | return; 117 | } 118 | 119 | ConfigurationSection configSectionData = oldBackupDataConfig.getConfigurationSection("data"); 120 | if (configSectionData == null) { 121 | return; 122 | } 123 | Set timestamps = configSectionData.getKeys(false); 124 | 125 | for (String timestampStr : timestamps) { 126 | try { 127 | Long timestamp = Long.parseLong(timestampStr); 128 | String fileName = backupFile.getName(); 129 | String fileUUIDStr = fileName.substring(0, fileName.indexOf('.')); 130 | 131 | UUID uuid; 132 | try { 133 | // Check if it's a valid UUID 134 | uuid = UUID.fromString(fileUUIDStr); 135 | } catch (IllegalArgumentException ex) { 136 | InventoryRollbackPlus.getInstance().getLogger().severe( 137 | "An error occurred when trying to retrieve the player UUID from " + backupFile.getAbsolutePath() + "#" + timestampStr + "! " + 138 | "Please ask for help in the issues section of the InventoryRollbackPlus github page."); 139 | ex.printStackTrace(); 140 | continue; 141 | } 142 | 143 | // ---- Load all data from the old config ---- 144 | 145 | String packageVersion = oldBackupDataConfig.getString("data." + timestamp + ".version"); 146 | ItemStack[] mainInvItems = RestoreInventory.getInventoryItems(packageVersion, 147 | oldBackupDataConfig.getString("data." + timestamp + ".inventory")); 148 | ItemStack[] armorItems = RestoreInventory.getInventoryItems(packageVersion, 149 | oldBackupDataConfig.getString("data." + timestamp + ".armour")); 150 | ItemStack[] enderChestItems = RestoreInventory.getInventoryItems(packageVersion, 151 | oldBackupDataConfig.getString("data." + timestamp + ".enderchest")); 152 | float xp = Float.parseFloat( 153 | oldBackupDataConfig.getString("data." + timestamp + ".xp")); 154 | double health = oldBackupDataConfig.getDouble("data." + timestamp + ".health"); 155 | int foodLevel = oldBackupDataConfig.getInt("data." + timestamp + ".hunger"); 156 | float saturation = Float.parseFloat( 157 | oldBackupDataConfig.getString("data." + timestamp + ".saturation")); 158 | String worldName = oldBackupDataConfig.getString("data." + timestamp + ".location.world"); 159 | double posX = oldBackupDataConfig.getDouble("data." + timestamp + ".location.x"); 160 | double posY = oldBackupDataConfig.getDouble("data." + timestamp + ".location.y"); 161 | double posZ = oldBackupDataConfig.getDouble("data." + timestamp + ".location.z"); 162 | String logTypeStoredString = oldBackupDataConfig.getString("data." + timestamp + ".logType"); 163 | String deathReason = oldBackupDataConfig.getString("data." + timestamp + ".deathReason"); 164 | 165 | // ---- Process all loaded data ---- 166 | 167 | PlayerData importedData = new PlayerData(uuid, logTypeProcessing, timestamp); 168 | 169 | importedData.setMainInventory(mainInvItems); 170 | importedData.setArmour(armorItems); 171 | importedData.setEnderChest(enderChestItems); 172 | importedData.setXP(xp); 173 | importedData.setHealth(health); 174 | importedData.setFoodLevel(foodLevel); 175 | importedData.setSaturation(saturation); 176 | importedData.setWorld(worldName); 177 | importedData.setX(posX); 178 | importedData.setY(posY); 179 | importedData.setZ(posZ); 180 | 181 | // Sanity check log type 182 | if (logTypeStoredString == null) { 183 | InventoryRollbackPlus.getInstance().getLogger().severe( 184 | "An error occurred when trying to retrieve the backup type of " + backupFile.getAbsolutePath() + "#" + timestampStr + "! " + 185 | "Please ask for help in the issues section of the InventoryRollbackPlus github page. (typeStr is null)"); 186 | continue; 187 | } 188 | 189 | // Convert old log type format to new enum value 190 | LogType logTypeStored = oldLogTypesMap.get(logTypeStoredString); 191 | 192 | // If this log type isn't an old type, attempt retrieval from newer LogType enum 193 | if (logTypeStored == null) { 194 | try { 195 | logTypeStored = LogType.valueOf(logTypeStoredString); 196 | } catch (IllegalArgumentException ex) { 197 | InventoryRollbackPlus.getInstance().getLogger().severe( 198 | "An error occurred when trying to retrieve the backup type of " + backupFile.getAbsolutePath() + "#" + timestampStr + "! " + 199 | "Please ask for help in the issues section of the InventoryRollbackPlus github page. (typeStr: " + logTypeStoredString + ")"); 200 | continue; 201 | } 202 | } 203 | 204 | // Apply last data after sanity checks & conversions 205 | importedData.setLogType(logTypeStored); 206 | importedData.setVersion(packageVersion); 207 | if (deathReason != null) importedData.setDeathReason(deathReason); 208 | 209 | // Save the data to the new folder location 210 | importedData.saveData(true); 211 | } catch (Exception e) { 212 | InventoryRollbackPlus.getPluginLogger().warning( 213 | MessageData.getPluginPrefix() + "Error converting backup file at " + 214 | backupFile.getAbsolutePath() + " on timestamp " + timestampStr); 215 | } 216 | } 217 | } 218 | 219 | public static YamlConfiguration loadConfiguration(@NotNull File file) { 220 | Validate.notNull(file, "File cannot be null"); 221 | YamlConfiguration config; 222 | 223 | try { 224 | config = new YamlConfiguration(); 225 | config.load(file); 226 | } catch (FileNotFoundException ignored) { 227 | return null; 228 | } catch (IOException | InvalidConfigurationException var4) { 229 | Bukkit.getLogger().severe("Cannot load " + file); 230 | return null; 231 | } 232 | 233 | return config; 234 | } 235 | 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/LogTimestamps.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util; 2 | 3 | import java.util.concurrent.ConcurrentLinkedQueue; 4 | 5 | public class LogTimestamps { 6 | 7 | public static final int MAX_SIZE = 5; 8 | 9 | private final ConcurrentLinkedQueue timestamps = new ConcurrentLinkedQueue<>(); 10 | 11 | public void log(Long timestamp) { 12 | timestamps.add(timestamp); 13 | while (timestamps.size() > MAX_SIZE) { 14 | timestamps.poll(); 15 | } 16 | } 17 | 18 | public Long getFirst() { 19 | return timestamps.peek(); 20 | } 21 | 22 | public boolean isFull() { 23 | return timestamps.size() >= MAX_SIZE; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/UserLogRateLimiter.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util; 2 | 3 | import me.danjono.inventoryrollback.data.LogType; 4 | 5 | import java.util.HashMap; 6 | 7 | public class UserLogRateLimiter { 8 | 9 | private static final long TICK_DURATION = 50; // ms 10 | private static final long BUFFER = Math.round(50 * 0.20d); // 10 ms - allow up to 20% faster than normal tick speeds 11 | private static final long MIN_INTERVAL_FROM_START_OF_LOG = (TICK_DURATION - BUFFER) * LogTimestamps.MAX_SIZE; // ms 12 | 13 | HashMap saveLogs = new HashMap<>(); 14 | 15 | public void log(LogType logType, Long timestamp) { 16 | LogTimestamps logTimestamps = saveLogs.get(logType); 17 | if (logTimestamps == null) { 18 | logTimestamps = new LogTimestamps(); 19 | saveLogs.put(logType, logTimestamps); 20 | } 21 | logTimestamps.log(timestamp); 22 | } 23 | 24 | public boolean isRateLimitExceeded(LogType logType) { 25 | LogTimestamps logTimestamps = saveLogs.get(logType); 26 | if (logTimestamps == null) { 27 | return false; 28 | } 29 | 30 | if (!logTimestamps.isFull()) { 31 | return false; 32 | } 33 | 34 | Long first = logTimestamps.getFirst(); 35 | if (first == null) { 36 | return false; 37 | } 38 | 39 | return System.currentTimeMillis() - first < MIN_INTERVAL_FROM_START_OF_LOG; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/DeserializationResult.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.serialization; 2 | 3 | import org.bukkit.inventory.ItemStack; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public class DeserializationResult { 7 | 8 | public static DeserializationResult failure(String errorMessage) { 9 | return new DeserializationResult(null, errorMessage); 10 | } 11 | 12 | private final ItemStack[] items; 13 | private final String errorMessage; 14 | 15 | public DeserializationResult(ItemStack[] items, String errorMessage) { 16 | this.items = items; 17 | this.errorMessage = errorMessage; 18 | } 19 | 20 | @Nullable 21 | public ItemStack[] getItems() { 22 | return items; 23 | } 24 | 25 | @Nullable 26 | public String getErrorMessage() { 27 | return errorMessage; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/ItemStackSerialization.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.serialization; 2 | 3 | import org.bukkit.inventory.ItemStack; 4 | 5 | import java.util.Base64; 6 | 7 | public class ItemStackSerialization { 8 | 9 | private static final String MODERN_SERIALIZATION_PREFIX = "IRP_VERSION:"; 10 | 11 | public static String serialize(ItemStack[] items) { 12 | // Always use the newest serialization format 13 | String serialized = Version3Serialization.serialize(items); 14 | String data = MODERN_SERIALIZATION_PREFIX + Version3Serialization.ID + ":" + serialized; 15 | return Base64.getEncoder().encodeToString(data.getBytes()); 16 | } 17 | 18 | public static DeserializationResult deserializeData(String packageVersion, String data) { 19 | if (data == null) { 20 | return new DeserializationResult(null, "Data is null"); 21 | } 22 | 23 | byte[] decodedBytes; 24 | String decodedString = null; 25 | 26 | try { 27 | decodedBytes = Base64.getDecoder().decode(data); 28 | decodedString = new String(decodedBytes); 29 | } catch (Exception ignored) {} 30 | 31 | // Default to version 1 if no prefix is found 32 | String version = "1"; 33 | String unprefixedData = data; // version 1 34 | 35 | // Modern serialization format: 36 | // IRP_VERSION:2:data-here 37 | if (decodedString != null && decodedString.startsWith(MODERN_SERIALIZATION_PREFIX)) { 38 | int prefixLen = MODERN_SERIALIZATION_PREFIX.length(); 39 | version = decodedString.substring(prefixLen, prefixLen + 1); 40 | unprefixedData = decodedString.substring(prefixLen + 2); 41 | } 42 | 43 | switch (version) { 44 | case "1": 45 | return new DeserializationResult( 46 | Version1Serialization.stacksFromBase64(packageVersion, unprefixedData), 47 | ""); 48 | case "2": 49 | return Version2Serialization.deserialize(unprefixedData); 50 | case "3": 51 | return Version3Serialization.deserialize(unprefixedData); 52 | default: 53 | return new DeserializationResult(null, "Unsupported serialization version: " + version); 54 | } 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/Version1Serialization.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.serialization; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import me.danjono.inventoryrollback.InventoryRollback; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import org.bukkit.ChatColor; 7 | import org.bukkit.inventory.ItemStack; 8 | import org.bukkit.util.io.BukkitObjectInputStream; 9 | import org.bukkit.util.io.BukkitObjectOutputStream; 10 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.ByteArrayOutputStream; 14 | import java.io.IOException; 15 | 16 | public class Version1Serialization { 17 | 18 | public static String toBase64(ItemStack[] contents) { 19 | boolean convert = false; 20 | 21 | for (ItemStack item : contents) { 22 | if (item != null) { 23 | convert = true; 24 | break; 25 | } 26 | } 27 | 28 | if (convert) { 29 | try { 30 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 31 | BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); 32 | 33 | dataOutput.writeInt(contents.length); 34 | 35 | for (ItemStack stack : contents) { 36 | dataOutput.writeObject(stack); 37 | } 38 | dataOutput.close(); 39 | byte[] byteArr = outputStream.toByteArray(); 40 | return Base64Coder.encodeLines(byteArr); 41 | } catch (Exception e) { 42 | throw new IllegalStateException("Unable to save item stacks.", e); 43 | } 44 | } 45 | 46 | return null; 47 | } 48 | 49 | public static ItemStack[] stacksFromBase64(String packageVersion, String data) { 50 | if (data == null) 51 | return new ItemStack[]{}; 52 | 53 | ByteArrayInputStream inputStream = null; 54 | 55 | try { 56 | inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data)); 57 | } catch (IllegalArgumentException e) { 58 | return new ItemStack[]{}; 59 | } 60 | 61 | BukkitObjectInputStream dataInput = null; 62 | ItemStack[] stacks = null; 63 | 64 | try { 65 | dataInput = new BukkitObjectInputStream(inputStream); 66 | stacks = new ItemStack[dataInput.readInt()]; 67 | } catch (IOException e1) { 68 | e1.printStackTrace(); 69 | } 70 | 71 | if (stacks == null) 72 | return new ItemStack[]{}; 73 | 74 | for (int i = 0; i < stacks.length; i++) { 75 | try { 76 | stacks[i] = (ItemStack) dataInput.readObject(); 77 | } catch (IOException | ClassNotFoundException | NullPointerException e) { 78 | //Backup generated before InventoryRollback v1.3 79 | if (packageVersion == null) { 80 | InventoryRollbackPlus.getPluginLogger().severe(ChatColor.stripColor(MessageData.getPluginPrefix()) + "There was an error deserializing the material data. This is likely caused by a now incompatible material ID if the backup was originally generated on a different Minecraft server version."); 81 | } 82 | //Backup was not generated on the same server version 83 | else if (!packageVersion.equalsIgnoreCase(InventoryRollbackPlus.getPackageVersion())) { 84 | InventoryRollbackPlus.getPluginLogger().severe(ChatColor.stripColor(MessageData.getPluginPrefix()) + "There was an error deserializing the material data. The backup was generated on a " + packageVersion + " version server whereas you are now running a " + InventoryRollback.getPackageVersion() + " version server. It is likely a material ID inside the backup is no longer valid on this Minecraft server version and cannot be convereted."); 85 | } 86 | //Unknown error 87 | else if (packageVersion.equalsIgnoreCase(InventoryRollbackPlus.getPackageVersion())) { 88 | InventoryRollbackPlus.getPluginLogger().severe(ChatColor.stripColor(MessageData.getPluginPrefix()) + "There was an error deserializing the material data. The data file is likely corrupted since this was saved on the same version the server is currently running on so it should have worked."); 89 | } 90 | 91 | try { 92 | dataInput.close(); 93 | } catch (IOException e1) { 94 | InventoryRollbackPlus.getPluginLogger().severe(ChatColor.stripColor(MessageData.getPluginPrefix()) + "There was an error while terminating read of backup data after an error already occurred."); 95 | } 96 | return null; 97 | } 98 | } 99 | 100 | try { 101 | dataInput.close(); 102 | } catch (IOException e1) { 103 | InventoryRollbackPlus.getPluginLogger().severe(ChatColor.stripColor(MessageData.getPluginPrefix()) + "There was an error while terminating read of backup data after normal read."); 104 | } 105 | 106 | return stacks; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/Version2Serialization.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.serialization; 2 | 3 | import org.bukkit.inventory.ItemStack; 4 | import org.bukkit.util.io.BukkitObjectInputStream; 5 | import org.bukkit.util.io.BukkitObjectOutputStream; 6 | 7 | import java.io.*; 8 | import java.util.Base64; 9 | 10 | public class Version2Serialization { 11 | 12 | public static final int ID = 2; 13 | 14 | public static DeserializationResult deserialize(String data) { 15 | try { 16 | byte[] b64decoded = Base64.getDecoder().decode(data); 17 | ByteArrayInputStream bais = new ByteArrayInputStream(b64decoded); 18 | return deserialize(bais); 19 | } catch (Exception e) { 20 | e.printStackTrace(); 21 | return new DeserializationResult(null, "Failed to deserialize item stack: " + e.getMessage()); 22 | } 23 | } 24 | 25 | public static DeserializationResult deserialize(InputStream bais) throws IOException { 26 | ItemStack[] items = new ItemStack[readInt(bais)]; 27 | 28 | for (int i = 0; i < items.length; i++) { 29 | // Read the length of the serialized item 30 | int length = readInt(bais); 31 | 32 | if (length == 0) { 33 | items[i] = null; 34 | continue; 35 | } 36 | 37 | // Read the serialized item 38 | byte[] serializedItem = new byte[length]; 39 | for (int j = 0; j < length; j++) { 40 | serializedItem[j] = (byte) bais.read(); 41 | } 42 | 43 | try { 44 | items[i] = deserializeItem(serializedItem); 45 | } catch (Exception ex) { 46 | ex.printStackTrace(); 47 | return new DeserializationResult(null, "Failed to deserialize item stack: " + ex.getMessage()); 48 | } 49 | } 50 | 51 | return new DeserializationResult(items, null); 52 | } 53 | 54 | public static String serialize(ItemStack[] items) { 55 | try { 56 | byte[] serializedBytes = serializeBytes(items); 57 | return Base64.getEncoder().encodeToString(serializedBytes); 58 | } catch (Exception e) { 59 | e.printStackTrace(); 60 | return null; 61 | } 62 | } 63 | 64 | public static byte[] serializeBytes(ItemStack[] items) throws IOException { 65 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 66 | 67 | // Write the number of items 68 | try { 69 | writeInt(baos, items.length); 70 | } catch (Exception ex) { 71 | ex.printStackTrace(); 72 | return null; 73 | } 74 | 75 | // Write each item 76 | for (ItemStack item : items) { 77 | // If the item is null, write a 0 int 78 | if (item == null) { 79 | writeInt(baos, 0); 80 | continue; 81 | } 82 | 83 | // Write the length of the serialized item followed by the serialized item 84 | try { 85 | byte[] serializedItem = serializeItem(item); 86 | writeInt(baos, serializedItem.length); 87 | baos.write(serializedItem); 88 | } catch (Exception ex) { 89 | ex.printStackTrace(); 90 | return null; 91 | } 92 | } 93 | 94 | try { 95 | baos.close(); 96 | } catch (IOException e) { 97 | e.printStackTrace(); 98 | return null; 99 | } 100 | 101 | return baos.toByteArray(); 102 | } 103 | 104 | private static byte[] serializeItem(ItemStack item) throws IOException { 105 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 106 | BukkitObjectOutputStream boos = new BukkitObjectOutputStream(baos); 107 | boos.writeObject(item); 108 | boos.close(); 109 | return baos.toByteArray(); 110 | } 111 | 112 | private static ItemStack deserializeItem(byte[] serialized) throws IOException, ClassNotFoundException { 113 | ByteArrayInputStream bais = new ByteArrayInputStream(serialized); 114 | BukkitObjectInputStream bois = new BukkitObjectInputStream(bais); 115 | return (ItemStack) bois.readObject(); 116 | } 117 | 118 | private static int readInt(InputStream is) throws IOException { 119 | int result = 0; 120 | for (int i = 0; i < 4; i++) { 121 | result |= (is.read() & 0xFF) << (i * 8); 122 | } 123 | return result; 124 | } 125 | 126 | private static void writeInt(OutputStream os, int value) throws IOException { 127 | for (int i = 0; i < 4; i++) { 128 | os.write((value >> (i * 8)) & 0xFF); 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/Version3Serialization.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.serialization; 2 | 3 | import org.bukkit.inventory.ItemStack; 4 | import org.bukkit.util.io.BukkitObjectInputStream; 5 | import org.bukkit.util.io.BukkitObjectOutputStream; 6 | 7 | import java.io.*; 8 | import java.util.Base64; 9 | import java.util.zip.GZIPInputStream; 10 | import java.util.zip.GZIPOutputStream; 11 | 12 | /** 13 | * This class handles the serialization and deserialization of ItemStacks 14 | * using a GZIP compressed format wrapped around Version 2. 15 | */ 16 | public class Version3Serialization { 17 | 18 | public static final int ID = 3; 19 | 20 | public static DeserializationResult deserialize(String data) { 21 | try { 22 | byte[] b64decoded = Base64.getDecoder().decode(data); 23 | ByteArrayInputStream bais = new ByteArrayInputStream(b64decoded); 24 | GZIPInputStream gis = new GZIPInputStream(bais); 25 | 26 | return Version2Serialization.deserialize(gis); 27 | } catch (Exception e) { 28 | e.printStackTrace(); 29 | return new DeserializationResult(null, "Failed to deserialize item stack: " + e.getMessage()); 30 | } 31 | } 32 | 33 | public static String serialize(ItemStack[] items) { 34 | try { 35 | byte[] serializedBytes = Version2Serialization.serializeBytes(items); 36 | 37 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 38 | GZIPOutputStream gos = new GZIPOutputStream(baos); 39 | 40 | assert serializedBytes != null; 41 | gos.write(serializedBytes); 42 | 43 | gos.close(); 44 | 45 | byte[] compressedBytes = baos.toByteArray(); 46 | return Base64.getEncoder().encodeToString(compressedBytes); 47 | } catch (Exception e) { 48 | e.printStackTrace(); 49 | return null; 50 | } 51 | } 52 | 53 | private static byte[] serializeItem(ItemStack item) throws IOException { 54 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 55 | BukkitObjectOutputStream boos = new BukkitObjectOutputStream(baos); 56 | boos.writeObject(item); 57 | boos.close(); 58 | return baos.toByteArray(); 59 | } 60 | 61 | private static ItemStack deserializeItem(byte[] serialized) throws IOException, ClassNotFoundException { 62 | ByteArrayInputStream bais = new ByteArrayInputStream(serialized); 63 | BukkitObjectInputStream bois = new BukkitObjectInputStream(bais); 64 | return (ItemStack) bois.readObject(); 65 | } 66 | 67 | private static int readInt(InputStream is) throws IOException { 68 | int result = 0; 69 | for (int i = 0; i < 4; i++) { 70 | result |= (is.read() & 0xFF) << (i * 8); 71 | } 72 | return result; 73 | } 74 | 75 | private static void writeInt(OutputStream os, int value) throws IOException { 76 | for (int i = 0; i < 4; i++) { 77 | os.write((value >> (i * 8)) & 0xFF); 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/test/SelfTest.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.test; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.tcoded.lightlibs.bukkitversion.MCVersion; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.function.Consumer; 9 | import java.util.logging.Level; 10 | import java.util.logging.Logger; 11 | 12 | public class SelfTest { 13 | 14 | private final String name; 15 | private final MCVersion minVersion; 16 | private final MCVersion maxVersion; 17 | private final Runnable test; 18 | private final List logs; 19 | 20 | public SelfTest(String name, Consumer> test) { 21 | this(name, MCVersion.v1_8_8, MCVersion.getLatest(), test); 22 | } 23 | 24 | public SelfTest(String name, MCVersion minVersion, Consumer> test) { 25 | this(name, minVersion, MCVersion.getLatest(), test); 26 | } 27 | 28 | public SelfTest(String name, MCVersion minVersion, MCVersion maxVersion, Consumer> test) { 29 | this.name = name; 30 | this.minVersion = minVersion; 31 | this.maxVersion = maxVersion; 32 | this.logs = new ArrayList<>(); 33 | this.test = () -> test.accept(logs); 34 | } 35 | 36 | public SelfTest(String name, Runnable test) { 37 | this(name, MCVersion.v1_8_8, MCVersion.getLatest(), test); 38 | } 39 | 40 | public SelfTest(String name, MCVersion minVersion, MCVersion maxVersion, Runnable test) { 41 | this.name = name; 42 | this.minVersion = minVersion; 43 | this.maxVersion = maxVersion; 44 | this.test = test; 45 | this.logs = new ArrayList<>(); 46 | } 47 | 48 | public String getName() { 49 | return name; 50 | } 51 | 52 | public void run() { 53 | try { 54 | test.run(); 55 | } catch (Throwable t) { 56 | Logger logger = InventoryRollbackPlus.getInstance().getLogger(); 57 | logger.log(Level.SEVERE, "Test failed with exception: " + test, t); 58 | 59 | logger.severe("Logs:"); 60 | for (String log : this.getLogs()) { 61 | logger.severe(" - " + log); 62 | } 63 | } 64 | } 65 | 66 | public List getLogs() { 67 | return logs; 68 | } 69 | 70 | public MCVersion getMinVersion() { 71 | return minVersion; 72 | } 73 | 74 | public MCVersion getMaxVersion() { 75 | return maxVersion; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/test/SelfTestSerialization.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.test; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.util.serialization.DeserializationResult; 5 | import com.nuclyon.technicallycoded.inventoryrollback.util.serialization.Version2Serialization; 6 | import com.tcoded.lightlibs.bukkitversion.MCVersion; 7 | import org.bukkit.Material; 8 | import org.bukkit.block.ShulkerBox; 9 | import org.bukkit.inventory.ItemStack; 10 | import org.bukkit.inventory.meta.BlockStateMeta; 11 | import org.bukkit.inventory.meta.ItemMeta; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.function.Consumer; 16 | import java.util.logging.Logger; 17 | 18 | public class SelfTestSerialization { 19 | 20 | public static void runTests() { 21 | List tests = Arrays.asList( 22 | SelfTestSerialization.buildTestSerializeAndDeserializeItem(), 23 | SelfTestSerialization.buildTestSerializeAndDeserializeEmptyInventory(), 24 | SelfTestSerialization.buildTestSerializeAndDeserializeFullInventory(), 25 | SelfTestSerialization.buildTestSerializeAndDeserializeShulkerBoxWithItems(), 26 | SelfTestSerialization.buildTestSerializeAndDeserializeCustomNamedItem() 27 | ); 28 | 29 | MCVersion currentVersion = InventoryRollbackPlus.getInstance().getVersion().getMcVersions()[0]; 30 | 31 | int completed = 0; 32 | int skipped = 0; 33 | int failed = 0; 34 | 35 | for (SelfTest test : tests) { 36 | if (currentVersion.greaterOrEqThan(test.getMinVersion()) && currentVersion.lessOrEqThan(test.getMaxVersion())) { 37 | try { 38 | test.run(); 39 | completed++; 40 | } catch (Exception e) { 41 | e.printStackTrace(); 42 | failed++; 43 | } 44 | } else { 45 | skipped++; 46 | } 47 | } 48 | 49 | Logger logger = InventoryRollbackPlus.getInstance().getLogger(); 50 | logger.info("Tests completed: " + completed + ", Skipped: " + skipped + ", Failed: " + failed); 51 | 52 | if (failed > 0) { 53 | logger.severe("Some tests failed. Please check the logs for details."); 54 | } else { 55 | logger.info("All tests passed successfully."); 56 | } 57 | 58 | } 59 | 60 | public static SelfTest buildTestSerializeAndDeserializeItem() { 61 | Runnable test = () -> { 62 | // Create a non-null item (requires a valid Bukkit Material environment) 63 | ItemStack original = new ItemStack(Material.DIRT, 10); 64 | ItemStack[] items = new ItemStack[]{original}; 65 | String serialized = Version2Serialization.serialize(items); 66 | TestAssertions.assertNotNull(serialized, "Serialized data should not be null"); 67 | 68 | DeserializationResult result = Version2Serialization.deserialize(serialized); 69 | TestAssertions.assertNull(result.getErrorMessage(), "There should be no error during deserialization"); 70 | TestAssertions.assertNotNull(result.getItems(), "The deserialized array should not be null"); 71 | TestAssertions.assertEquals(1, result.getItems().length, "The deserialized array should have one item"); 72 | 73 | ItemStack deserialized = result.getItems()[0]; 74 | TestAssertions.assertNotNull(deserialized, "Deserialized item should not be null"); 75 | TestAssertions.assertEquals(original.getType(), deserialized.getType(), "Item types should be equal"); 76 | TestAssertions.assertEquals(original.getAmount(), deserialized.getAmount(), "Item amounts should be equal"); 77 | }; 78 | 79 | return new SelfTest("Serialize and Deserialize Item", test); 80 | } 81 | 82 | public static SelfTest buildTestSerializeAndDeserializeEmptyInventory() { 83 | Runnable test = () -> { 84 | ItemStack[] items = new ItemStack[0]; 85 | String serialized = Version2Serialization.serialize(items); 86 | TestAssertions.assertNotNull(serialized, "Serialized data should not be null"); 87 | 88 | DeserializationResult result = Version2Serialization.deserialize(serialized); 89 | TestAssertions.assertNull(result.getErrorMessage(), "There should be no error during deserialization"); 90 | TestAssertions.assertNotNull(result.getItems(), "The deserialized array should not be null"); 91 | TestAssertions.assertEquals(0, result.getItems().length, "The deserialized array should be empty"); 92 | }; 93 | 94 | return new SelfTest("Serialize and Deserialize Empty Inventory", test); 95 | } 96 | 97 | public static SelfTest buildTestSerializeAndDeserializeFullInventory() { 98 | Runnable test = () -> { 99 | ItemStack[] items = new ItemStack[36]; 100 | for (int i = 0; i < items.length; i++) { 101 | items[i] = new ItemStack(Material.STONE, i + 1); 102 | } 103 | String serialized = Version2Serialization.serialize(items); 104 | TestAssertions.assertNotNull(serialized, "Serialized data should not be null"); 105 | 106 | DeserializationResult result = Version2Serialization.deserialize(serialized); 107 | TestAssertions.assertNull(result.getErrorMessage(), "There should be no error during deserialization"); 108 | TestAssertions.assertNotNull(result.getItems(), "The deserialized array should not be null"); 109 | TestAssertions.assertEquals(36, result.getItems().length, "The deserialized array should have 36 items"); 110 | 111 | for (int i = 0; i < items.length; i++) { 112 | TestAssertions.assertEquals(items[i].getType(), result.getItems()[i].getType(), "Item types should match"); 113 | TestAssertions.assertEquals(items[i].getAmount(), result.getItems()[i].getAmount(), "Item amounts should match"); 114 | } 115 | }; 116 | 117 | return new SelfTest("Serialize and Deserialize Full Inventory", test); 118 | } 119 | 120 | public static SelfTest buildTestSerializeAndDeserializeShulkerBoxWithItems() { 121 | Consumer> test = logs -> { 122 | ItemStack shulkerBox = new ItemStack(Material.SHULKER_BOX); 123 | 124 | // Add items to the shulker box (requires a valid Bukkit API environment) 125 | ItemStack[] shulkerContents = new ItemStack[] { 126 | new ItemStack(Material.DIAMOND, 5), 127 | new ItemStack(Material.GOLD_INGOT, 10) 128 | }; 129 | 130 | if (shulkerBox.getItemMeta() instanceof BlockStateMeta) { 131 | BlockStateMeta meta = (BlockStateMeta) shulkerBox.getItemMeta(); 132 | if (meta.getBlockState() instanceof ShulkerBox) { 133 | ShulkerBox shulker = (ShulkerBox) meta.getBlockState(); 134 | shulker.getSnapshotInventory().setContents(shulkerContents); 135 | meta.setBlockState(shulker); 136 | } else { 137 | throw new IllegalStateException("BlockState is not ShulkerBox"); 138 | } 139 | shulkerBox.setItemMeta(meta); 140 | } else { 141 | throw new IllegalStateException("ItemMeta is not BlockStateMeta"); 142 | } 143 | 144 | logs.add("shulkerBox = " + shulkerBox); 145 | 146 | 147 | ItemStack[] items = new ItemStack[]{shulkerBox}; 148 | String serialized = Version2Serialization.serialize(items); 149 | TestAssertions.assertNotNull(serialized, "Serialized data should not be null"); 150 | 151 | logs.add("serialized = " + serialized); 152 | 153 | DeserializationResult result = Version2Serialization.deserialize(serialized); 154 | TestAssertions.assertNull(result.getErrorMessage(), "There should be no error during deserialization"); 155 | TestAssertions.assertNotNull(result.getItems(), "The deserialized array should not be null"); 156 | TestAssertions.assertEquals(1, result.getItems().length, "The deserialized array should have one item"); 157 | 158 | ItemStack deserializedShulker = result.getItems()[0]; 159 | TestAssertions.assertNotNull(deserializedShulker, "Deserialized shulker box should not be null"); 160 | TestAssertions.assertEquals(Material.SHULKER_BOX, deserializedShulker.getType(), "Item type should be SHULKER_BOX"); 161 | 162 | logs.add("deserializedShulker = " + deserializedShulker); 163 | 164 | // Verify shulker box contents 165 | ItemStack[] deserializedContents; 166 | if (deserializedShulker.getItemMeta() instanceof BlockStateMeta) { 167 | BlockStateMeta meta = (BlockStateMeta) deserializedShulker.getItemMeta(); 168 | if (meta.getBlockState() instanceof ShulkerBox) { 169 | ShulkerBox shulker = (ShulkerBox) meta.getBlockState(); 170 | deserializedContents = shulker.getInventory().getContents(); 171 | } else { 172 | throw new IllegalStateException("BlockState is not ShulkerBox"); 173 | } 174 | } else { 175 | throw new IllegalStateException("ItemMeta is not BlockStateMeta"); 176 | } 177 | 178 | logs.add("deserializedContents = " + Arrays.toString(deserializedContents)); 179 | 180 | int itemCount = 0; 181 | for (ItemStack item : deserializedContents) { 182 | if (item != null) itemCount++; 183 | } 184 | logs.add("itemCount = " + itemCount); 185 | 186 | TestAssertions.assertEquals(2, itemCount, "Shulker box should contain 2 items"); 187 | TestAssertions.assertEquals(shulkerContents[0].getType(), deserializedContents[0].getType(), "First item type should match"); 188 | TestAssertions.assertEquals(shulkerContents[0].getAmount(), deserializedContents[0].getAmount(), "First item amount should match"); 189 | TestAssertions.assertEquals(shulkerContents[1].getType(), deserializedContents[1].getType(), "Second item type should match"); 190 | TestAssertions.assertEquals(shulkerContents[1].getAmount(), deserializedContents[1].getAmount(), "Second item amount should match"); 191 | }; 192 | 193 | return new SelfTest("Serialize and Deserialize Shulker Box with Items", MCVersion.v1_11, test); 194 | } 195 | 196 | public static SelfTest buildTestSerializeAndDeserializeCustomNamedItem() { 197 | Runnable test = () -> { 198 | ItemStack customItem = new ItemStack(Material.DIAMOND_SWORD); 199 | // Set a custom name for the item 200 | ItemMeta itemMeta = customItem.getItemMeta(); 201 | itemMeta.setDisplayName("Excalibur"); 202 | customItem.setItemMeta(itemMeta); 203 | 204 | ItemStack[] items = new ItemStack[]{customItem}; 205 | String serialized = Version2Serialization.serialize(items); 206 | TestAssertions.assertNotNull(serialized, "Serialized data should not be null"); 207 | 208 | DeserializationResult result = Version2Serialization.deserialize(serialized); 209 | TestAssertions.assertNull(result.getErrorMessage(), "There should be no error during deserialization"); 210 | TestAssertions.assertNotNull(result.getItems(), "The deserialized array should not be null"); 211 | TestAssertions.assertEquals(1, result.getItems().length, "The deserialized array should have one item"); 212 | 213 | ItemStack deserializedItem = result.getItems()[0]; 214 | TestAssertions.assertNotNull(deserializedItem, "Deserialized item should not be null"); 215 | TestAssertions.assertEquals(Material.DIAMOND_SWORD, deserializedItem.getType(), "Item type should be DIAMOND_SWORD"); 216 | TestAssertions.assertEquals("Excalibur", deserializedItem.getItemMeta().getDisplayName(), "Custom name should match"); 217 | }; 218 | 219 | return new SelfTest("Serialize and Deserialize Custom Named Item", test); 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/com/nuclyon/technicallycoded/inventoryrollback/util/test/TestAssertions.java: -------------------------------------------------------------------------------- 1 | package com.nuclyon.technicallycoded.inventoryrollback.util.test; 2 | 3 | public class TestAssertions { 4 | 5 | public static void assertNull(Object obj, String error) { 6 | if (obj != null) { 7 | throw new AssertionError(error); 8 | } 9 | } 10 | 11 | public static void assertNotNull(Object obj, String error) { 12 | if (obj == null) { 13 | throw new AssertionError(error); 14 | } 15 | } 16 | 17 | public static void assertEquals(Object expected, Object actual, String error) { 18 | if ((expected == null && actual != null) || (expected != null && !expected.equals(actual))) { 19 | throw new AssertionError(error); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/InventoryRollback.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import me.danjono.inventoryrollback.UpdateChecker.UpdateResult; 5 | import me.danjono.inventoryrollback.commands.Commands; 6 | import me.danjono.inventoryrollback.config.ConfigData; 7 | import me.danjono.inventoryrollback.config.ConfigData.SaveType; 8 | import me.danjono.inventoryrollback.config.MessageData; 9 | import me.danjono.inventoryrollback.config.SoundData; 10 | import me.danjono.inventoryrollback.data.MySQL; 11 | import me.danjono.inventoryrollback.data.YAML; 12 | import me.danjono.inventoryrollback.listeners.ClickGUI; 13 | import me.danjono.inventoryrollback.listeners.EventLogs; 14 | import org.bukkit.Bukkit; 15 | import org.bukkit.ChatColor; 16 | import org.bukkit.plugin.java.JavaPlugin; 17 | 18 | import java.sql.SQLException; 19 | import java.util.Objects; 20 | import java.util.logging.Level; 21 | import java.util.logging.Logger; 22 | 23 | public abstract class InventoryRollback extends JavaPlugin { 24 | 25 | private static final Logger logger = Logger.getLogger("Minecraft"); 26 | private static InventoryRollback instance; 27 | private static String packageVersion; 28 | 29 | public static Logger getPluginLogger() { 30 | return logger; 31 | } 32 | 33 | public static void setInstance(InventoryRollback plugin) { 34 | instance = plugin; 35 | } 36 | 37 | public static InventoryRollback getInstance() { 38 | return InventoryRollbackPlus.getInstance(); 39 | } 40 | 41 | public static void setPackageVersion(String version) { 42 | packageVersion = version; 43 | } 44 | 45 | public static String getPackageVersion() { 46 | return packageVersion; 47 | } 48 | 49 | public static String getPluginVersion() { 50 | return instance.getDescription().getVersion(); 51 | } 52 | 53 | @Override 54 | public void onEnable() { 55 | // !!!! WARNING !!!! This method is never used since it's overridden by the InventoryRollbackPlus onEnable() 56 | setInstance(this); 57 | setPackageVersion(Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3]); 58 | 59 | // if (!isCompatibleCb()) { 60 | // logger.log(Level.WARNING, MessageData.getPluginPrefix() + ChatColor.RED + " ** WARNING... Plugin may not be compatible with this version of Minecraft. **"); 61 | // logger.log(Level.WARNING, MessageData.getPluginPrefix() + ChatColor.RED + " ** Please fully test the plugin before using on your server as features may be broken. **"); 62 | // } 63 | 64 | startupTasks(); 65 | 66 | if (ConfigData.isbStatsEnabled()) 67 | bStats(); 68 | 69 | Objects.requireNonNull(this.getCommand("inventoryrollback")).setExecutor(new Commands()); 70 | 71 | this.getServer().getPluginManager().registerEvents(new ClickGUI(), instance); 72 | this.getServer().getPluginManager().registerEvents(new EventLogs(), instance); 73 | } 74 | 75 | @Override 76 | public void onDisable() { 77 | setInstance(null); 78 | } 79 | 80 | public void startupTasks() { 81 | 82 | if (ConfigData.getSaveType() == SaveType.YAML) { 83 | YAML.createStorageFolders(); 84 | } else if (ConfigData.getSaveType() == SaveType.MYSQL) { 85 | try { 86 | new MySQL(null, null, (long) 0).createTables(); 87 | } catch (SQLException e) { 88 | e.printStackTrace(); 89 | } 90 | } 91 | 92 | new MessageData().setMessages(); 93 | new SoundData().setSounds(); 94 | 95 | InventoryRollbackPlus.getInstance().getConsoleSender().sendMessage(MessageData.getPluginPrefix() + "Inventory backup data is set to save to: " + ConfigData.getSaveType().getName()); 96 | 97 | if (ConfigData.isUpdateCheckerEnabled()) 98 | getInstance().checkUpdate(); 99 | } 100 | 101 | /*public enum CompatibleVersions { 102 | V1_8_R1, 103 | V1_8_R2, 104 | V1_8_R3, 105 | V1_9_R1, 106 | V1_9_R2, 107 | V1_10_R1, 108 | V1_11_R1, 109 | V1_12_R1, 110 | V1_13_R1, 111 | V1_13_R2, 112 | V1_14_R1, 113 | V1_15_R1, 114 | V1_16_R1, 115 | V1_16_R2, 116 | V1_16_R3 117 | }*/ 118 | 119 | /*public enum VersionName { 120 | V1_8, 121 | V1_9_V1_12, 122 | V1_13_PLUS 123 | }*/ 124 | 125 | // private static EnumNmsVersion version = EnumNmsVersion.v1_13_R1; 126 | 127 | // public abstract void setVersion(EnumNmsVersion versionName); 128 | /*{ 129 | version = versionName; 130 | }*/ 131 | 132 | // public abstract EnumNmsVersion getVersion(); 133 | /*{ 134 | return version; 135 | }*/ 136 | 137 | public abstract boolean isCompatibleCb(String cbVersion); 138 | /* { 139 | for (CompatibleVersions v : CompatibleVersions.values()) { 140 | if (v.name().equalsIgnoreCase(packageVersion)) { 141 | //Check if 1.8 142 | if (v.name().equalsIgnoreCase("v1_8_R1") 143 | || v.name().equalsIgnoreCase("v1_8_R2") 144 | || v.name().equalsIgnoreCase("v1_8_R3")) { 145 | setVersion(VersionName.V1_8); 146 | } 147 | //Check if 1.9 - 1.12.2 148 | else if (v.name().equalsIgnoreCase("v1_9_R1") 149 | || v.name().equalsIgnoreCase("v1_9_R2") 150 | || v.name().equalsIgnoreCase("v1_10_R1") 151 | || v.name().equalsIgnoreCase("v1_11_R1") 152 | || v.name().equalsIgnoreCase("v1_12_R1")) { 153 | setVersion(VersionName.V1_9_V1_12); 154 | } 155 | //Else it is 1.13+ 156 | return true; 157 | } 158 | } 159 | 160 | return false; 161 | }*/ 162 | 163 | public void bStats() { 164 | // Override by IRP 165 | } 166 | 167 | public void checkUpdate() { 168 | Bukkit.getScheduler().runTaskAsynchronously(InventoryRollback.getInstance(), () -> { 169 | logger.log(Level.INFO, MessageData.getPluginPrefix() + "Checking for updates..."); 170 | 171 | final UpdateResult result = new UpdateChecker(getInstance(), 85811).getResult(); 172 | 173 | switch (result) { 174 | case FAIL_SPIGOT: 175 | logger.log(Level.INFO, MessageData.getPluginPrefix() + "Could not contact Spigot to check if an update is available."); 176 | break; 177 | case UPDATE_AVAILABLE: 178 | logger.log(Level.INFO, ChatColor.AQUA + "======================================================================================"); 179 | logger.log(Level.INFO, ChatColor.AQUA + "An update to InventoryRollbackPlus is available!"); 180 | logger.log(Level.INFO, ChatColor.AQUA + "Download at https://www.spigotmc.org/resources/inventoryrollback-plus-1-8-1-16-x.85811/"); 181 | logger.log(Level.INFO, ChatColor.AQUA + "======================================================================================"); 182 | break; 183 | case NO_UPDATE: 184 | logger.log(Level.INFO, MessageData.getPluginPrefix() + ChatColor.AQUA + "You are running the latest version."); 185 | break; 186 | default: 187 | break; 188 | } 189 | }); 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/UpdateChecker.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.net.MalformedURLException; 7 | import java.net.URL; 8 | import java.net.URLConnection; 9 | 10 | import org.bukkit.plugin.java.JavaPlugin; 11 | 12 | public class UpdateChecker { 13 | 14 | //Credit to PatoTheBest and TRollStar12345 on SpigotMC for the below code 15 | //https://www.spigotmc.org/threads/resource-updater-for-your-plugins-v1-1.37315/ 16 | //https://www.spigotmc.org/threads/check-for-updates-using-the-new-spigot-api.266310/ 17 | 18 | private JavaPlugin plugin; 19 | private URL checkURL; 20 | 21 | private String currentVersion; 22 | private String availableVersion; 23 | 24 | private UpdateResult result = UpdateResult.FAIL_SPIGOT; 25 | 26 | public enum UpdateResult { 27 | NO_UPDATE, 28 | FAIL_SPIGOT, 29 | UPDATE_AVAILABLE 30 | } 31 | 32 | public UpdateChecker(JavaPlugin plugin, Integer resourceId) { 33 | this.plugin = plugin; 34 | this.currentVersion = regex(this.plugin.getDescription().getVersion()); 35 | 36 | try { 37 | this.checkURL = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + resourceId); 38 | } catch (MalformedURLException e) { 39 | result = UpdateResult.FAIL_SPIGOT; 40 | return; 41 | } 42 | 43 | run(); 44 | } 45 | 46 | private void run() { 47 | URLConnection con = null; 48 | try { 49 | con = checkURL.openConnection(); 50 | } catch (IOException e1) { 51 | result = UpdateResult.FAIL_SPIGOT; 52 | return; 53 | } 54 | 55 | try { 56 | availableVersion = regex(new BufferedReader(new InputStreamReader(con.getInputStream())).readLine()); 57 | } catch (IOException e) { 58 | result = UpdateResult.FAIL_SPIGOT; 59 | return; 60 | } 61 | 62 | if (availableVersion.isEmpty()) { 63 | result = UpdateResult.FAIL_SPIGOT; 64 | return; 65 | } else if (availableVersion.equalsIgnoreCase(currentVersion)) { 66 | result = UpdateResult.NO_UPDATE; 67 | return; 68 | } else if (!availableVersion.equalsIgnoreCase(currentVersion)) { 69 | result = UpdateResult.UPDATE_AVAILABLE; 70 | return; 71 | } 72 | 73 | result = UpdateResult.FAIL_SPIGOT; 74 | 75 | } 76 | 77 | public UpdateResult getResult() { 78 | return this.result; 79 | } 80 | 81 | public String getVersion() { 82 | return regex(this.availableVersion); 83 | } 84 | 85 | private String regex(String version) { 86 | return version.replaceAll("[^0-9.]", ""); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/config/SoundData.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.config; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.tcoded.lightlibs.bukkitversion.BukkitVersion; 5 | import org.bukkit.Sound; 6 | 7 | public class SoundData extends ConfigData { 8 | 9 | private InventoryRollbackPlus main; 10 | 11 | private static Sound teleport; 12 | private static boolean teleportEnabled; 13 | 14 | private static Sound inventoryRestored; 15 | private static boolean inventoryRestoreEnabled; 16 | 17 | private static Sound foodRestored; 18 | private static boolean foodRestoredEnabled; 19 | 20 | private static Sound hungerRestored; 21 | private static boolean hungerRestoredEnabled; 22 | 23 | private static Sound experienceRestored; 24 | private static boolean experienceRestoredEnabled; 25 | 26 | public SoundData() { 27 | this.main = InventoryRollbackPlus.getInstance(); 28 | } 29 | 30 | public void setSounds() { 31 | //If sounds are invalid they will be disabled. 32 | try { 33 | setTeleport(Sound.ENTITY_ENDERMAN_TELEPORT); 34 | } catch (NoSuchFieldError e) { 35 | if (this.main.getVersion().lessOrEqThan(BukkitVersion.v1_8_R3)) { 36 | setTeleport(Sound.valueOf("ENDERMAN_TELEPORT")); 37 | } else if (this.main.getVersion().isWithin(BukkitVersion.v1_9_R1, BukkitVersion.v1_12_R1)) { 38 | setTeleport(Sound.valueOf("ENTITY_ENDERMEN_TELEPORT")); 39 | } 40 | } 41 | 42 | if (teleport != null) 43 | setTeleportEnabled((boolean) getDefaultValue("sounds.teleport.enabled", true)); 44 | 45 | try { 46 | setInvetoryRestored(Sound.ENTITY_ENDER_DRAGON_FLAP); 47 | } catch (NoSuchFieldError e) { 48 | if (this.main.getVersion().lessOrEqThan(BukkitVersion.v1_8_R3)) { 49 | setInvetoryRestored(Sound.valueOf("ENDERDRAGON_WINGS")); 50 | } else if (this.main.getVersion().isWithin(BukkitVersion.v1_9_R1, BukkitVersion.v1_12_R1)) { 51 | setInvetoryRestored(Sound.valueOf("ENTITY_ENDERDRAGON_FLAP")); 52 | } 53 | } 54 | 55 | if (inventoryRestored != null) 56 | setInventoryRestoredEnabled((boolean) getDefaultValue("sounds.inventory.enabled", true)); 57 | 58 | try { 59 | setFoodRestored(Sound.ENTITY_GENERIC_EAT); 60 | } catch (NoSuchFieldError e) { 61 | if (this.main.getVersion().lessOrEqThan(BukkitVersion.v1_8_R3)) { 62 | setFoodRestored(Sound.valueOf("EAT")); 63 | } else if (this.main.getVersion().isWithin(BukkitVersion.v1_9_R1, BukkitVersion.v1_12_R1)) { 64 | setFoodRestored(Sound.valueOf("ENTITY_GENERIC_EAT")); 65 | } 66 | } 67 | 68 | if (foodRestored != null) 69 | setFoodRestoredEnabled((boolean) getDefaultValue("sounds.food.enabled", true)); 70 | 71 | try { 72 | setHungerRestored(Sound.ENTITY_HORSE_EAT); 73 | } catch (NoSuchFieldError e) { 74 | if (this.main.getVersion().lessOrEqThan(BukkitVersion.v1_8_R3)) { 75 | setHungerRestored(Sound.valueOf("HORSE_IDLE")); 76 | } else if (this.main.getVersion().isWithin(BukkitVersion.v1_9_R1, BukkitVersion.v1_12_R1)) { 77 | setHungerRestored(Sound.valueOf("ENTITY_HORSE_EAT")); 78 | } 79 | } 80 | 81 | if (hungerRestored != null) 82 | setHungerRestoredEnabled((boolean) getDefaultValue("sounds.hunger.enabled", true)); 83 | 84 | try { 85 | setExperienceSound(Sound.ENTITY_PLAYER_LEVELUP); 86 | } catch (NoSuchFieldError e) { 87 | if (this.main.getVersion().lessOrEqThan(BukkitVersion.v1_8_R3)) { 88 | setExperienceSound(Sound.valueOf("LEVEL_UP")); 89 | } else if (this.main.getVersion().isWithin(BukkitVersion.v1_9_R1, BukkitVersion.v1_12_R1)) { 90 | setExperienceSound(Sound.valueOf("ENTITY_PLAYER_LEVELUP")); 91 | } 92 | } 93 | 94 | if (experienceRestored != null) 95 | setExperienceRestoredEnabled((boolean) getDefaultValue("sounds.xp.enabled", true)); 96 | 97 | } 98 | 99 | public static void setTeleport(Sound value) { 100 | teleport = value; 101 | } 102 | 103 | public static void setTeleportEnabled(boolean value) { 104 | teleportEnabled = value; 105 | } 106 | 107 | public static void setInvetoryRestored(Sound value) { 108 | inventoryRestored = value; 109 | } 110 | 111 | public static void setInventoryRestoredEnabled(boolean value) { 112 | inventoryRestoreEnabled = value; 113 | } 114 | 115 | public static void setFoodRestored(Sound value) { 116 | foodRestored = value; 117 | } 118 | 119 | public static void setFoodRestoredEnabled(boolean value) { 120 | foodRestoredEnabled = value; 121 | } 122 | 123 | public static void setHungerRestored(Sound value) { 124 | hungerRestored = value; 125 | } 126 | 127 | public static void setHungerRestoredEnabled(boolean value) { 128 | hungerRestoredEnabled = value; 129 | } 130 | 131 | public static void setExperienceSound(Sound value) { 132 | experienceRestored = value; 133 | } 134 | 135 | public static void setExperienceRestoredEnabled(boolean value) { 136 | experienceRestoredEnabled = value; 137 | } 138 | 139 | public static Sound getTeleport() { 140 | return teleport; 141 | } 142 | 143 | public static boolean isTeleportEnabled() { 144 | return teleportEnabled; 145 | } 146 | 147 | public static Sound getInventoryRestored() { 148 | return inventoryRestored; 149 | } 150 | 151 | public static boolean isInventoryRestoreEnabled() { 152 | return inventoryRestoreEnabled; 153 | } 154 | 155 | public static Sound getFoodRestored() { 156 | return foodRestored; 157 | } 158 | 159 | public static boolean isFoodRestoredEnabled() { 160 | return foodRestoredEnabled; 161 | } 162 | 163 | public static Sound getHungerRestored() { 164 | return hungerRestored; 165 | } 166 | 167 | public static boolean isHungerRestoredEnabled() { 168 | return hungerRestoredEnabled; 169 | } 170 | 171 | public static Sound getExperienceSound() { 172 | return experienceRestored; 173 | } 174 | 175 | public static boolean isExperienceRestoredEnabled() { 176 | return experienceRestoredEnabled; 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/data/LogType.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.data; 2 | 3 | public enum LogType { 4 | JOIN, 5 | QUIT, 6 | DEATH, 7 | WORLD_CHANGE, 8 | FORCE, 9 | UNKNOWN; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/gui/InventoryName.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.gui; 2 | 3 | import me.danjono.inventoryrollback.config.ConfigData; 4 | 5 | public enum InventoryName { 6 | 7 | MAIN_MENU("Inventory Rollback", 36), 8 | PLAYER_MENU("Player Data", 9), 9 | ROLLBACK_LIST("Rollbacks", ConfigData.getBackupLinesVisible() * 9 + 9), 10 | MAIN_BACKUP("Main Inventory Backup", 54), 11 | ENDER_CHEST_BACKUP("Ender Chest Backup", 36); 12 | 13 | private final String menuName; 14 | private final int size; 15 | 16 | private InventoryName(String name, int size) { 17 | this.menuName = name; 18 | this.size = size; 19 | } 20 | 21 | public String getName() { 22 | return this.menuName; 23 | } 24 | 25 | public int getSize() { 26 | return this.size; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/gui/menu/EnderChestBackupMenu.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.gui.menu; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import me.danjono.inventoryrollback.config.ConfigData; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import me.danjono.inventoryrollback.data.LogType; 7 | import me.danjono.inventoryrollback.data.PlayerData; 8 | import me.danjono.inventoryrollback.gui.Buttons; 9 | import me.danjono.inventoryrollback.gui.InventoryName; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.inventory.Inventory; 13 | import org.bukkit.inventory.ItemStack; 14 | import org.bukkit.scheduler.BukkitRunnable; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | public class EnderChestBackupMenu { 21 | 22 | private int pageNumber; 23 | 24 | private Player staff; 25 | private UUID playerUUID; 26 | private LogType logType; 27 | private Long timestamp; 28 | private ItemStack[] enderchest; 29 | 30 | private Buttons buttons; 31 | private Inventory inventory; 32 | 33 | public EnderChestBackupMenu(Player staff, PlayerData data, int pageNumberIn) { 34 | this.staff = staff; 35 | this.playerUUID = data.getOfflinePlayer().getUniqueId(); 36 | this.logType = data.getLogType(); 37 | this.timestamp = data.getTimestamp(); 38 | this.enderchest = data.getEnderChest(); 39 | this.pageNumber = pageNumberIn; 40 | this.buttons = new Buttons(playerUUID); 41 | 42 | createInventory(); 43 | } 44 | 45 | public void createInventory() { 46 | inventory = Bukkit.createInventory(staff, InventoryName.ENDER_CHEST_BACKUP.getSize(), InventoryName.ENDER_CHEST_BACKUP.getName()); 47 | 48 | List lore = new ArrayList<>(); 49 | if (pageNumber == 1) { 50 | ItemStack mainInventoryMenu = buttons.inventoryMenuBackButton(MessageData.getBackButton(), logType, timestamp); 51 | inventory.setItem(InventoryName.ENDER_CHEST_BACKUP.getSize() - 8, mainInventoryMenu); 52 | } 53 | 54 | if (pageNumber > 1) { 55 | lore.add("Page " + (pageNumber - 1)); 56 | ItemStack previousPage = buttons.enderChestBackButton(MessageData.getPreviousPageButton(), logType, pageNumber - 1, timestamp, lore); 57 | 58 | inventory.setItem(InventoryName.ENDER_CHEST_BACKUP.getSize() - 8, previousPage); 59 | } 60 | } 61 | 62 | public Inventory getInventory() { 63 | return this.inventory; 64 | } 65 | 66 | public void showEnderChestItems() { 67 | //Check how many items there are in total 68 | if (enderchest == null) enderchest = new ItemStack[0]; 69 | int itemsToDisplay = enderchest.length; 70 | 71 | // How many rows are available 72 | int spaceAvailable = InventoryName.ROLLBACK_LIST.getSize() - 9; 73 | 74 | // How many pages are required 75 | int pagesRequired = Math.max(1, (int) Math.ceil(itemsToDisplay / (double) spaceAvailable)); 76 | 77 | //Check if pageNumber supplied is greater then pagesRequired, if true set to last page 78 | if (pageNumber > pagesRequired) { 79 | pageNumber = pagesRequired; 80 | } else if (pageNumber <= 0) { 81 | pageNumber = 1; 82 | } 83 | 84 | //If the backup file is invalid it will return null, we want to catch it here 85 | try { 86 | 87 | // Add items, 5 per tick 88 | new BukkitRunnable() { 89 | 90 | int invPosition = 0; 91 | int itemPos = (pageNumber - 1) * 27; 92 | final int max = Math.max(0, itemPos + Math.min(enderchest.length - itemPos, 27)); // excluded but starts from 0 93 | 94 | @Override 95 | public void run() { 96 | for (int i = 0; i < 6; i++) { 97 | // If hit max item position, stop 98 | if (itemPos >= max) { 99 | this.cancel(); 100 | return; 101 | } 102 | 103 | 104 | ItemStack itemStack = enderchest[itemPos]; 105 | if (itemStack != null) { 106 | inventory.setItem(invPosition, itemStack); 107 | // Don't change inv position if there was nothing to place 108 | invPosition++; 109 | } 110 | // Move to next item stack 111 | itemPos++; 112 | } 113 | } 114 | }.runTaskTimer(InventoryRollbackPlus.getInstance(), 0, 1); 115 | } catch (NullPointerException e) { 116 | staff.sendMessage(MessageData.getPluginPrefix() + MessageData.getErrorInventory()); 117 | return; 118 | } 119 | 120 | // Add restore all player inventory button 121 | if (ConfigData.isRestoreToPlayerButton()) { 122 | inventory.setItem( 123 | InventoryName.ENDER_CHEST_BACKUP.getSize() - 5, 124 | buttons.restoreAllInventory(logType, timestamp)); 125 | } else { 126 | inventory.setItem( 127 | InventoryName.ENDER_CHEST_BACKUP.getSize() - 5, 128 | buttons.restoreAllInventoryDisabled(logType, timestamp)); 129 | } 130 | 131 | 132 | List lore = new ArrayList<>(); 133 | if (pageNumber < pagesRequired) { 134 | lore.add("Page " + (pageNumber + 1)); 135 | ItemStack nextPage = buttons.enderChestNextButton(MessageData.getNextPageButton(), logType, pageNumber + 1, timestamp, lore); 136 | 137 | inventory.setItem(InventoryName.ENDER_CHEST_BACKUP.getSize() - 2, nextPage); 138 | lore.clear(); 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/gui/menu/MainInventoryBackupMenu.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.gui.menu; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import me.danjono.inventoryrollback.config.ConfigData; 5 | import me.danjono.inventoryrollback.config.MessageData; 6 | import me.danjono.inventoryrollback.data.LogType; 7 | import me.danjono.inventoryrollback.data.PlayerData; 8 | import me.danjono.inventoryrollback.gui.Buttons; 9 | import me.danjono.inventoryrollback.gui.InventoryName; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.inventory.Inventory; 13 | import org.bukkit.inventory.ItemStack; 14 | import org.bukkit.scheduler.BukkitRunnable; 15 | 16 | import java.util.UUID; 17 | import java.util.concurrent.ExecutionException; 18 | import java.util.concurrent.Future; 19 | 20 | public class MainInventoryBackupMenu { 21 | 22 | private final InventoryRollbackPlus main; 23 | 24 | private final Player staff; 25 | private final UUID playerUUID; 26 | private final LogType logType; 27 | private final Long timestamp; 28 | private final ItemStack[] mainInventory; 29 | private final ItemStack[] armour; 30 | private final ItemStack[] enderChest; 31 | private final String location; 32 | private final double health; 33 | private final int hunger; 34 | private final float saturation; 35 | private final float xp; 36 | 37 | private final Buttons buttons; 38 | private Inventory inventory; 39 | 40 | private int mainInvLen; 41 | 42 | public MainInventoryBackupMenu(Player staff, PlayerData data, String location) { 43 | this.main = InventoryRollbackPlus.getInstance(); 44 | 45 | this.staff = staff; 46 | this.playerUUID = data.getOfflinePlayer().getUniqueId(); 47 | this.logType = data.getLogType(); 48 | this.timestamp = data.getTimestamp(); 49 | this.mainInventory = data.getMainInventory(); 50 | this.armour = data.getArmour(); 51 | this.enderChest = data.getEnderChest(); 52 | this.location = location; 53 | this.health = data.getHealth(); 54 | this.hunger = data.getFoodLevel(); 55 | this.saturation = data.getSaturation(); 56 | this.xp = data.getXP(); 57 | 58 | this.buttons = new Buttons(playerUUID); 59 | 60 | this.mainInvLen = mainInventory == null ? 0 : mainInventory.length; 61 | 62 | createInventory(); 63 | } 64 | 65 | public void createInventory() { 66 | inventory = Bukkit.createInventory(staff, InventoryName.MAIN_BACKUP.getSize(), InventoryName.MAIN_BACKUP.getName()); 67 | 68 | //Add back button 69 | inventory.setItem(46, buttons.inventoryMenuBackButton(MessageData.getBackButton(), logType, timestamp)); 70 | } 71 | 72 | public Inventory getInventory() { 73 | return this.inventory; 74 | } 75 | 76 | public void showBackupItems() { 77 | // Make sure we are not running this on the main thread 78 | assert !Bukkit.isPrimaryThread(); 79 | 80 | int item = 0; 81 | int position = 0; 82 | 83 | //If the backup file is invalid it will return null, we want to catch it here 84 | try { 85 | // Add items, 5 per tick 86 | new BukkitRunnable() { 87 | 88 | int invPosition = 0; 89 | int itemPos = 0; 90 | final int max = mainInvLen - 5; // excluded 91 | 92 | @Override 93 | public void run() { 94 | for (int i = 0; i < 6; i++) { 95 | // If hit max item position, stop 96 | if (itemPos >= max) { 97 | this.cancel(); 98 | return; 99 | } 100 | 101 | ItemStack itemStack = mainInventory[itemPos]; 102 | if (itemStack != null) { 103 | inventory.setItem(invPosition, itemStack); 104 | // Don't change inv position if there was nothing to place 105 | invPosition++; 106 | } 107 | // Move to next item stack 108 | itemPos++; 109 | } 110 | } 111 | }.runTaskTimer(main, 0, 1); 112 | } catch (Exception ex) { 113 | ex.printStackTrace(); 114 | staff.sendMessage(MessageData.getPluginPrefix() + MessageData.getErrorInventory()); 115 | return; 116 | } 117 | 118 | item = 36; 119 | position = 44; 120 | 121 | //Add armour 122 | if (armour != null && armour.length > 0) { 123 | try { 124 | for (int i = 0; i < armour.length; i++) { 125 | // Place item safely 126 | final int finalPos = position; 127 | final int finalItem = i; 128 | Future placeItemFuture = main.getServer().getScheduler().callSyncMethod(main, 129 | () -> { 130 | inventory.setItem(finalPos, armour[finalItem]); 131 | return null; 132 | }); 133 | placeItemFuture.get(); 134 | position--; 135 | } 136 | } catch (ExecutionException | InterruptedException ex) { 137 | ex.printStackTrace(); 138 | } 139 | } else { 140 | try { 141 | for (int i = 36; i < mainInvLen; i++) { 142 | if (mainInventory[item] != null) { 143 | // Place item safely 144 | final int finalPos = position; 145 | final int finalItem = item; 146 | Future placeItemFuture = main.getServer().getScheduler().callSyncMethod(main, 147 | () -> { 148 | inventory.setItem(finalPos, mainInventory[finalItem]); 149 | return null; 150 | }); 151 | placeItemFuture.get(); 152 | position--; 153 | } 154 | item++; 155 | } 156 | } catch (ExecutionException | InterruptedException ex) { 157 | ex.printStackTrace(); 158 | } 159 | } 160 | 161 | // Add restore all player inventory button 162 | if (ConfigData.isRestoreToPlayerButton()) 163 | inventory.setItem(48, buttons.restoreAllInventory(logType, timestamp)); 164 | else 165 | inventory.setItem(48, buttons.restoreAllInventoryDisabled(logType, timestamp)); 166 | 167 | //Add teleport back button 168 | inventory.setItem(49, buttons.enderPearlButton(logType, location)); 169 | 170 | //Add Enderchest icon 171 | inventory.setItem(50, buttons.enderChestButton(logType, timestamp, enderChest)); 172 | 173 | //Add health icon 174 | inventory.setItem(51, buttons.healthButton(logType, health)); 175 | 176 | //Add hunger icon 177 | inventory.setItem(52, buttons.hungerButton(logType, hunger, saturation)); 178 | 179 | //Add Experience Bottle 180 | inventory.setItem(53, buttons.experiencePotion(logType, xp)); 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/gui/menu/MainMenu.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.gui.menu; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.inventory.Inventory; 9 | import org.bukkit.inventory.ItemStack; 10 | 11 | import me.danjono.inventoryrollback.config.MessageData; 12 | import me.danjono.inventoryrollback.data.LogType; 13 | import me.danjono.inventoryrollback.gui.Buttons; 14 | import me.danjono.inventoryrollback.gui.InventoryName; 15 | 16 | public class MainMenu { 17 | 18 | private Player staff; 19 | 20 | private int pagesRequired; 21 | private int pageNumber; 22 | private Buttons buttons; 23 | 24 | private int startSelection; 25 | private int playerHeadLoops; 26 | 27 | private List onlinePlayers; 28 | private Inventory inventory; 29 | 30 | public MainMenu(Player staff, int pageNumber) { 31 | this.staff = staff; 32 | this.pageNumber = pageNumber; 33 | this.buttons = new Buttons(staff.getUniqueId()); 34 | 35 | getPlayerHeadData(); 36 | createInventory(); 37 | } 38 | 39 | public void createInventory() { 40 | inventory = Bukkit.createInventory(staff, InventoryName.MAIN_MENU.getSize(), InventoryName.MAIN_MENU.getName()); 41 | 42 | List lore = new ArrayList<>(); 43 | if (pageNumber > 1) { 44 | lore.add("Page " + (pageNumber - 1)); 45 | ItemStack previousPage = buttons.backButton(MessageData.getPreviousPageButton(), LogType.UNKNOWN, pageNumber - 1, lore); 46 | 47 | inventory.setItem(InventoryName.MAIN_MENU.getSize() - 8, previousPage); 48 | lore.clear(); 49 | } 50 | } 51 | 52 | public Inventory getInventory() { 53 | return this.inventory; 54 | } 55 | 56 | public void getMainMenu() { 57 | int selection = startSelection; 58 | for (int i = 0; i < playerHeadLoops; i++) { 59 | Player player = onlinePlayers.get(selection); 60 | Buttons playerButton = new Buttons(player); 61 | 62 | inventory.setItem(i, playerButton.playerHead(null, true)); 63 | selection++; 64 | } 65 | 66 | List lore = new ArrayList<>(); 67 | if (pageNumber < pagesRequired) { 68 | lore.add("Page " + (pageNumber + 1)); 69 | ItemStack nextPage = buttons.nextButton(MessageData.getNextPageButton(), LogType.UNKNOWN, pageNumber + 1, lore); 70 | 71 | inventory.setItem(InventoryName.MAIN_MENU.getSize() - 2, nextPage); 72 | lore.clear(); 73 | } 74 | } 75 | 76 | public void getPlayerHeadData() { 77 | //Get current online players 78 | onlinePlayers = new ArrayList<>(Bukkit.getOnlinePlayers()); 79 | 80 | //Check how many online players there are in total 81 | int playersOnline = onlinePlayers.size(); 82 | 83 | //How many rows are required 84 | int spaceRequired = InventoryName.MAIN_MENU.getSize() - 9; 85 | 86 | //How many pages are required 87 | pagesRequired = (int) Math.ceil(playersOnline / (double) spaceRequired); 88 | 89 | //Check if pageNumber supplied is greater then pagesRequired, if true set to last page 90 | if (pageNumber > pagesRequired) { 91 | pageNumber = pagesRequired; 92 | } else if (pageNumber <= 0) { 93 | pageNumber = 1; 94 | } 95 | 96 | //Get the amount of players that will show on this page 97 | startSelection = (((InventoryName.MAIN_MENU.getSize() - 9) * pageNumber) - (InventoryName.MAIN_MENU.getSize() - 9)); 98 | 99 | int variance = playersOnline - (spaceRequired + startSelection); 100 | 101 | if (variance > 0) { 102 | playerHeadLoops = spaceRequired; 103 | } else { 104 | playerHeadLoops = spaceRequired + variance; 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/gui/menu/PlayerMenu.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.gui.menu; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.ChatColor; 10 | import org.bukkit.OfflinePlayer; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.inventory.Inventory; 13 | 14 | import me.danjono.inventoryrollback.config.MessageData; 15 | import me.danjono.inventoryrollback.data.LogType; 16 | import me.danjono.inventoryrollback.data.PlayerData; 17 | import me.danjono.inventoryrollback.gui.Buttons; 18 | import me.danjono.inventoryrollback.gui.InventoryName; 19 | 20 | public class PlayerMenu { 21 | 22 | private Player staff; 23 | private OfflinePlayer offlinePlayer; 24 | 25 | private Buttons buttons; 26 | private Inventory inventory; 27 | 28 | public PlayerMenu(Player staff, OfflinePlayer player) { 29 | this.staff = staff; 30 | this.offlinePlayer = player; 31 | this.buttons = new Buttons(player.getUniqueId()); 32 | 33 | createInventory(); 34 | } 35 | 36 | public void createInventory() { 37 | inventory = Bukkit.createInventory(staff, InventoryName.PLAYER_MENU.getSize(), InventoryName.PLAYER_MENU.getName()); 38 | 39 | inventory.setItem(2, buttons.createDeathLogButton(LogType.DEATH, null)); 40 | inventory.setItem(3, buttons.createJoinLogButton(LogType.JOIN, null)); 41 | inventory.setItem(4, buttons.createQuitLogButton(LogType.QUIT, null)); 42 | inventory.setItem(5, buttons.createWorldChangeLogButton(LogType.WORLD_CHANGE, null)); 43 | inventory.setItem(6, buttons.createForceSaveLogButton(LogType.FORCE, null)); 44 | } 45 | 46 | public Inventory getInventory() { 47 | return this.inventory; 48 | } 49 | 50 | public void getPlayerMenu() { 51 | List lore = new ArrayList<>(); 52 | 53 | if (offlinePlayer.isOnline()) { 54 | lore.add(ChatColor.GREEN + "Online now"); 55 | } else if (!offlinePlayer.hasPlayedBefore()) { 56 | lore.add(ChatColor.RED + "Never played on this server"); 57 | } else { 58 | lore.add(ChatColor.RED + "Offline"); 59 | 60 | String dateTime = "Unknown"; 61 | if (offlinePlayer.getLastPlayed() != 0) 62 | dateTime = PlayerData.getTime(offlinePlayer.getLastPlayed()); 63 | lore.add(ChatColor.RED + "Last online: " + dateTime); 64 | } 65 | 66 | inventory.setItem(0, buttons.playerHead(lore, true)); 67 | UUID uuid = offlinePlayer.getUniqueId(); 68 | 69 | PlayerData deathBackup = new PlayerData(uuid, LogType.DEATH, null); 70 | PlayerData joinBackup = new PlayerData(uuid, LogType.JOIN, null); 71 | PlayerData quitBackup = new PlayerData(uuid, LogType.QUIT, null); 72 | PlayerData worldChangeBackup = new PlayerData(uuid, LogType.WORLD_CHANGE, null); 73 | PlayerData forceSaveBackup = new PlayerData(uuid, LogType.FORCE, null); 74 | 75 | if (!joinBackup.doesBackupTypeExist() 76 | && !quitBackup.doesBackupTypeExist() 77 | && !deathBackup.doesBackupTypeExist() 78 | && !worldChangeBackup.doesBackupTypeExist() 79 | && !forceSaveBackup.doesBackupTypeExist()) { 80 | 81 | //No backups have been found for the player 82 | staff.sendMessage(MessageData.getPluginPrefix() + MessageData.getNoBackupError(offlinePlayer.getName())); 83 | } 84 | 85 | String backupsAvailable = " backup(s) available"; 86 | 87 | List deaths = Arrays.asList(deathBackup.getAmountOfBackups() + backupsAvailable); 88 | inventory.setItem(2, buttons.createDeathLogButton(LogType.DEATH, deaths)); 89 | 90 | List joins = Arrays.asList(joinBackup.getAmountOfBackups() + backupsAvailable); 91 | inventory.setItem(3, buttons.createJoinLogButton(LogType.JOIN, joins)); 92 | 93 | List quits = Arrays.asList(quitBackup.getAmountOfBackups() + backupsAvailable); 94 | inventory.setItem(4, buttons.createQuitLogButton(LogType.QUIT, quits)); 95 | 96 | List worldChange = Arrays.asList(worldChangeBackup.getAmountOfBackups() + backupsAvailable); 97 | inventory.setItem(5, buttons.createWorldChangeLogButton(LogType.WORLD_CHANGE, worldChange)); 98 | 99 | List forceSaves = Arrays.asList(forceSaveBackup.getAmountOfBackups() + backupsAvailable); 100 | inventory.setItem(6, buttons.createForceSaveLogButton(LogType.FORCE, forceSaves)); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/gui/menu/RollbackListMenu.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.gui.menu; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.Material; 9 | import org.bukkit.OfflinePlayer; 10 | import org.bukkit.entity.Player; 11 | import org.bukkit.inventory.Inventory; 12 | import org.bukkit.inventory.ItemStack; 13 | 14 | import me.danjono.inventoryrollback.config.MessageData; 15 | import me.danjono.inventoryrollback.data.LogType; 16 | import me.danjono.inventoryrollback.data.PlayerData; 17 | import me.danjono.inventoryrollback.gui.Buttons; 18 | import me.danjono.inventoryrollback.gui.InventoryName; 19 | 20 | public class RollbackListMenu { 21 | 22 | private int pageNumber; 23 | 24 | private Player staff; 25 | private UUID playerUUID; 26 | private LogType logType; 27 | 28 | private Buttons buttons; 29 | private Inventory inventory; 30 | 31 | public RollbackListMenu(Player staff, OfflinePlayer player, LogType logType, int pageNumberIn) { 32 | this.staff = staff; 33 | this.playerUUID = player.getUniqueId(); 34 | this.logType = logType; 35 | this.pageNumber = pageNumberIn; 36 | this.buttons = new Buttons(playerUUID); 37 | 38 | createInventory(); 39 | } 40 | 41 | public void createInventory() { 42 | inventory = Bukkit.createInventory(staff, InventoryName.ROLLBACK_LIST.getSize(), InventoryName.ROLLBACK_LIST.getName()); 43 | 44 | List lore = new ArrayList<>(); 45 | if (pageNumber == 1) { 46 | ItemStack mainMenu = buttons.backButton(MessageData.getMainMenuButton(), logType, 0, null); 47 | inventory.setItem(InventoryName.ROLLBACK_LIST.getSize() - 8, mainMenu); 48 | } 49 | 50 | if (pageNumber > 1) { 51 | lore.add("Page " + (pageNumber - 1)); 52 | ItemStack previousPage = buttons.backButton(MessageData.getPreviousPageButton(), logType, pageNumber - 1, lore); 53 | 54 | inventory.setItem(InventoryName.ROLLBACK_LIST.getSize() - 8, previousPage); 55 | lore.clear(); 56 | } 57 | } 58 | 59 | public Inventory getInventory() { 60 | return this.inventory; 61 | } 62 | 63 | public void showBackups() { 64 | PlayerData playerData = new PlayerData(playerUUID, logType, null); 65 | 66 | //Check how many backups there are in total 67 | int backups = playerData.getAmountOfBackups(); 68 | 69 | //How many rows are required 70 | int spaceRequired = InventoryName.ROLLBACK_LIST.getSize() - 9; 71 | 72 | //How many pages are required 73 | int pagesRequired = (int) Math.ceil(backups / (double) spaceRequired); 74 | 75 | //Check if pageNumber supplied is greater than pagesRequired, if true set to last page 76 | if (pageNumber > pagesRequired) { 77 | pageNumber = pagesRequired; 78 | } else if (pageNumber <= 0) { 79 | pageNumber = 1; 80 | } 81 | 82 | int backupsAlreadyPassed = spaceRequired * (pageNumber - 1); 83 | int backupsOnCurrentPage = Math.min(backups, Math.min(spaceRequired, backups - backupsAlreadyPassed)); 84 | List timeStamps = playerData.getSelectedPageTimestamps(pageNumber); 85 | 86 | int position = 0; 87 | for (int i = 0; i < backupsOnCurrentPage; i++) { 88 | try { 89 | Long timestamp = timeStamps.get(i); 90 | playerData = new PlayerData(playerUUID, logType, timestamp); 91 | 92 | playerData.getRollbackMenuData(); 93 | 94 | String displayName = MessageData.getDeathTime(PlayerData.getTime(timestamp)); 95 | 96 | List lore = new ArrayList<>(); 97 | 98 | String deathReason = playerData.getDeathReason(); 99 | if (deathReason != null) 100 | lore.add(MessageData.getDeathReason(deathReason)); 101 | 102 | String world = playerData.getWorld(); 103 | double x = playerData.getX(); 104 | double y = playerData.getY(); 105 | double z = playerData.getZ(); 106 | String location = world + "," + x + "," + y + "," + z; 107 | 108 | lore.add(MessageData.getDeathLocationWorld(world)); 109 | lore.add(MessageData.getDeathLocationX(x)); 110 | lore.add(MessageData.getDeathLocationY(y)); 111 | lore.add(MessageData.getDeathLocationZ(z)); 112 | 113 | ItemStack item = buttons.createInventoryButton(new ItemStack(Material.CHEST), logType, location, timestamp, displayName, lore); 114 | 115 | inventory.setItem(position, item); 116 | 117 | } catch (IndexOutOfBoundsException e) { 118 | e.printStackTrace(); 119 | } 120 | 121 | position++; 122 | } 123 | 124 | List lore = new ArrayList<>(); 125 | if (pageNumber < pagesRequired) { 126 | lore.add("Page " + (pageNumber + 1)); 127 | ItemStack nextPage = buttons.nextButton(MessageData.getNextPageButton(), logType, pageNumber + 1, lore); 128 | 129 | inventory.setItem(position + 7, nextPage); 130 | lore.clear(); 131 | } 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/inventory/RestoreInventory.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.inventory; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.util.serialization.ItemStackSerialization; 4 | import org.bukkit.entity.Player; 5 | import org.bukkit.inventory.ItemStack; 6 | 7 | import java.math.BigDecimal; 8 | 9 | public class RestoreInventory { 10 | 11 | private RestoreInventory() { 12 | throw new IllegalStateException("Restore inventory class"); 13 | } 14 | 15 | public static ItemStack[] getInventoryItems(String packageVersion, String base64) { 16 | ItemStack[] inv = null; 17 | 18 | try { 19 | inv = ItemStackSerialization.deserializeData(packageVersion, base64).getItems(); 20 | } catch (IllegalArgumentException e) { 21 | e.printStackTrace(); 22 | } 23 | 24 | return inv; 25 | } 26 | 27 | //Credits to Dev_Richard (https://www.spigotmc.org/members/dev_richard.38792/) 28 | //https://gist.github.com/RichardB122/8958201b54d90afbc6f0 29 | public static void setTotalExperience(Player player, float xpFloat) { 30 | int xp = (int) xpFloat; 31 | 32 | //Levels 0 through 15 33 | if (xp >= 0 && xp <= 351) { 34 | //Calculate Everything 35 | int a = 1; 36 | int b = 6; 37 | int c = -xp; 38 | int level = (int) (-b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a); 39 | int xpForLevel = (int) (Math.pow(level, 2) + (6 * level)); 40 | int remainder = xp - xpForLevel; 41 | int experienceNeeded = (2 * level) + 7; 42 | float experience = (float) remainder / (float) experienceNeeded; 43 | experience = round(experience, 2); 44 | 45 | //Set Everything 46 | player.setLevel(level); 47 | player.setExp(experience); 48 | //Levels 16 through 30 49 | } else if (xp >= 352 && xp < 1507) { 50 | //Calculate Everything 51 | double a = 2.5; 52 | double b = -40.5; 53 | int c = -xp + 360; 54 | double dLevel = (-b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a); 55 | int level = (int) Math.floor(dLevel); 56 | int xpForLevel = (int) (2.5 * Math.pow(level, 2) - (40.5 * level) + 360); 57 | int remainder = xp - xpForLevel; 58 | int experienceNeeded = (5 * level) - 38; 59 | float experience = (float) remainder / (float) experienceNeeded; 60 | experience = round(experience, 2); 61 | 62 | //Set Everything 63 | player.setLevel(level); 64 | player.setExp(experience); 65 | //Level 31 and greater 66 | } else { 67 | //Calculate Everything 68 | double a = 4.5; 69 | double b = -162.5; 70 | int c = -xp + 2220; 71 | double dLevel = (-b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a); 72 | int level = (int) Math.floor(dLevel); 73 | int xpForLevel = (int) (4.5 * Math.pow(level, 2) - (162.5 * level) + 2220); 74 | int remainder = xp - xpForLevel; 75 | int experienceNeeded = (9 * level) - 158; 76 | float experience = (float) remainder / (float) experienceNeeded; 77 | experience = round(experience, 2); 78 | 79 | //Set Everything 80 | player.setLevel(level); 81 | player.setExp(experience); 82 | } 83 | } 84 | 85 | public static float getLevel(float floatXP) { 86 | int xp = (int) floatXP; 87 | 88 | //Levels 0 through 15 89 | if (xp >= 0 && xp < 351) { 90 | //Calculate Everything 91 | int a = 1; 92 | int b = 6; 93 | int c = -xp; 94 | int level = (int) (-b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a); 95 | int xpForLevel = (int) (Math.pow(level, 2) + (6 * level)); 96 | int remainder = xp - xpForLevel; 97 | int experienceNeeded = (2 * level) + 7; 98 | float experience = (float) remainder / (float) experienceNeeded; 99 | experience = round(experience, 2); 100 | 101 | return level; 102 | //Levels 16 through 30 103 | } else if (xp >= 352 && xp < 1507) { 104 | //Calculate Everything 105 | double a = 2.5; 106 | double b = -40.5; 107 | int c = -xp + 360; 108 | double dLevel = (-b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a); 109 | int level = (int) Math.floor(dLevel); 110 | int xpForLevel = (int) (2.5 * Math.pow(level, 2) - (40.5 * level) + 360); 111 | int remainder = xp - xpForLevel; 112 | int experienceNeeded = (5 * level) - 38; 113 | float experience = (float) remainder / (float) experienceNeeded; 114 | experience = round(experience, 2); 115 | 116 | //Set Everything 117 | return level; 118 | //Level 31 and greater 119 | } else { 120 | //Calculate Everything 121 | double a = 4.5; 122 | double b = -162.5; 123 | int c = -xp + 2220; 124 | double dLevel = (-b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a); 125 | int level = (int) Math.floor(dLevel); 126 | int xpForLevel = (int) (4.5 * Math.pow(level, 2) - (162.5 * level) + 2220); 127 | int remainder = xp - xpForLevel; 128 | int experienceNeeded = (9 * level) - 158; 129 | float experience = (float) remainder / (float) experienceNeeded; 130 | experience = round(experience, 2); 131 | 132 | //Set Everything 133 | return (float) level; 134 | } 135 | } 136 | 137 | private static float round(float d, int decimalPlace) { 138 | BigDecimal bd = BigDecimal.valueOf((double) d); 139 | bd = bd.setScale(decimalPlace, BigDecimal.ROUND_HALF_DOWN); 140 | return bd.floatValue(); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/inventory/SaveInventory.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.inventory; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.util.UserLogRateLimiter; 5 | import com.nuclyon.technicallycoded.inventoryrollback.util.serialization.ItemStackSerialization; 6 | import com.tcoded.lightlibs.bukkitversion.BukkitVersion; 7 | import me.danjono.inventoryrollback.InventoryRollback; 8 | import me.danjono.inventoryrollback.data.LogType; 9 | import me.danjono.inventoryrollback.data.PlayerData; 10 | import org.bukkit.Location; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.event.entity.EntityDamageEvent.DamageCause; 13 | import org.bukkit.inventory.Inventory; 14 | import org.bukkit.inventory.ItemStack; 15 | import org.bukkit.inventory.PlayerInventory; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.util.Arrays; 19 | import java.util.HashMap; 20 | import java.util.UUID; 21 | import java.util.concurrent.CompletableFuture; 22 | 23 | public class SaveInventory { 24 | 25 | private static final HashMap rateLimiters = new HashMap<>(); 26 | 27 | private final InventoryRollbackPlus main; 28 | 29 | private final long timestamp; 30 | private final Player player; 31 | private final LogType logType; 32 | private final DamageCause deathCause; 33 | private final String causeAlias; 34 | 35 | public SaveInventory(Player player, LogType logType, DamageCause deathCause, String causeAliasIn) { 36 | this.main = InventoryRollbackPlus.getInstance(); 37 | this.timestamp = System.currentTimeMillis(); 38 | this.player = player; 39 | this.logType = logType; 40 | this.deathCause = deathCause; 41 | this.causeAlias = causeAliasIn; 42 | } 43 | 44 | public void snapshotAndSave(PlayerInventory mainInventory, Inventory enderChestInventory, boolean saveAsync) { 45 | PlayerDataSnapshot snapshot = createSnapshot(mainInventory, enderChestInventory); 46 | if (snapshot == null) return; 47 | 48 | save(snapshot, saveAsync); 49 | } 50 | 51 | public void save(PlayerDataSnapshot snapshot, boolean async) { 52 | if (snapshot == null) return; 53 | UUID uuid = player.getUniqueId(); 54 | 55 | // Rate limiter 56 | UserLogRateLimiter userLogRateLimiter = rateLimiters.get(uuid); 57 | if (userLogRateLimiter == null) { 58 | userLogRateLimiter = new UserLogRateLimiter(); 59 | rateLimiters.put(uuid, userLogRateLimiter); 60 | } 61 | userLogRateLimiter.log(logType, timestamp); 62 | if (userLogRateLimiter.isRateLimitExceeded(logType)) { 63 | main.getLogger().warning("Player " + player.getName() + " is being rate limited! This means that something is causing this log to be created FASTER than even once per tick! Log type: " + logType.name()); 64 | new IllegalStateException("Rate limiting reached! This should never happen under normal operation!").printStackTrace(); 65 | return; 66 | } 67 | 68 | boolean saveAsync = !InventoryRollbackPlus.getInstance().isShuttingDown() && async; 69 | Runnable saveTask = () -> { 70 | PlayerData data = new PlayerData(player, logType, timestamp); 71 | 72 | if (snapshot.finalMainInvContents != null) data.setMainInventory(snapshot.finalMainInvContents); 73 | if (snapshot.finalMainInvArmor != null) data.setArmour(snapshot.finalMainInvArmor); 74 | if (snapshot.finalEnderInvContents != null) data.setEnderChest(snapshot.finalEnderInvContents); 75 | 76 | data.setXP(snapshot.totalXp); 77 | data.setHealth(snapshot.health); 78 | data.setFoodLevel(snapshot.foodLevel); 79 | data.setSaturation(snapshot.saturation); 80 | data.setWorld(snapshot.worldName); 81 | 82 | data.setX(snapshot.locX); 83 | data.setY(snapshot.locY); 84 | data.setZ(snapshot.locZ); 85 | 86 | data.setLogType(logType); 87 | data.setVersion(InventoryRollback.getPackageVersion()); 88 | 89 | if (causeAlias != null) data.setDeathReason(causeAlias); 90 | else if (deathCause != null) data.setDeathReason(deathCause.name()); 91 | else if (logType == LogType.DEATH) data.setDeathReason("UNKNOWN"); 92 | 93 | // Remove excess saves if limit is reached 94 | CompletableFuture purgeTask = data.purgeExcessSaves(saveAsync); 95 | 96 | // Save new data 97 | purgeTask.thenRun(() -> data.saveData(saveAsync)); 98 | }; 99 | 100 | if (saveAsync) main.getServer().getScheduler().runTaskAsynchronously(main, saveTask); 101 | else saveTask.run(); 102 | 103 | } 104 | 105 | public @Nullable PlayerDataSnapshot createSnapshot(PlayerInventory mainInventory, Inventory enderChestInventory) { 106 | ItemStack[] mainInvContents = null; 107 | ItemStack[] mainInvArmor = null; 108 | ItemStack[] enderInvContents = null; 109 | 110 | for (ItemStack item : mainInventory.getContents()) { 111 | // If any item is not null, we take a copy of the contents 112 | if (item != null) { 113 | mainInvContents = copyItemArray(mainInventory.getContents()); 114 | break; 115 | } 116 | } 117 | 118 | if (main.getVersion().lessOrEqThan(BukkitVersion.v1_8_R3)) { 119 | for (ItemStack item : mainInventory.getArmorContents()) { 120 | // If any item is not null, we take a copy of the contents 121 | if (item != null) { 122 | mainInvArmor = copyItemArray(mainInventory.getArmorContents()); 123 | break; 124 | } 125 | } 126 | } 127 | 128 | for (ItemStack item : enderChestInventory.getContents()) { 129 | // If any item is not null, we take a copy of the contents 130 | if (item != null) { 131 | enderInvContents = copyItemArray(enderChestInventory.getContents()); 132 | break; 133 | } 134 | } 135 | 136 | float totalXp = getTotalExperience(player); 137 | double health = player.getHealth(); 138 | int foodLevel = player.getFoodLevel(); 139 | float saturation = player.getSaturation(); 140 | String worldName = player.getWorld().getName(); 141 | 142 | // Location data 143 | Location pLoc = player.getLocation(); 144 | // Multiply by 10, truncate, divide by 10 145 | // This has the effect of only keeping 1 decimal of precision 146 | double locX = ((int)(pLoc.getX() * 10)) / 10d; 147 | double locY = ((int)(pLoc.getY() * 10)) / 10d; 148 | double locZ = ((int)(pLoc.getZ() * 10)) / 10d; 149 | 150 | // Final vars 151 | ItemStack[] finalMainInvContents = mainInvContents; 152 | ItemStack[] finalMainInvArmor = mainInvArmor; 153 | ItemStack[] finalEnderInvContents = enderInvContents; 154 | 155 | return new PlayerDataSnapshot(totalXp, health, foodLevel, saturation, worldName, locX, locY, locZ, 156 | finalMainInvContents, finalMainInvArmor, finalEnderInvContents); 157 | } 158 | 159 | private ItemStack[] copyItemArray(ItemStack[] contents) { 160 | ItemStack[] copy = new ItemStack[contents.length]; 161 | for (int i = 0; i < contents.length; i++) { 162 | if (contents[i] != null) { 163 | copy[i] = contents[i].clone(); 164 | } 165 | } 166 | return copy; 167 | } 168 | 169 | public static class PlayerDataSnapshot { 170 | public final float totalXp; 171 | public final double health; 172 | public final int foodLevel; 173 | public final float saturation; 174 | public final String worldName; 175 | public final double locX; 176 | public final double locY; 177 | public final double locZ; 178 | public final ItemStack[] finalMainInvContents; 179 | public final ItemStack[] finalMainInvArmor; 180 | public final ItemStack[] finalEnderInvContents; 181 | 182 | public PlayerDataSnapshot(float totalXp, double health, int foodLevel, float saturation, String worldName, double locX, double locY, double locZ, ItemStack[] finalMainInvContents, ItemStack[] finalMainInvArmor, ItemStack[] finalEnderInvContents) { 183 | this.totalXp = totalXp; 184 | this.health = health; 185 | this.foodLevel = foodLevel; 186 | this.saturation = saturation; 187 | this.worldName = worldName; 188 | this.locX = locX; 189 | this.locY = locY; 190 | this.locZ = locZ; 191 | this.finalMainInvContents = finalMainInvContents; 192 | this.finalMainInvArmor = finalMainInvArmor; 193 | this.finalEnderInvContents = finalEnderInvContents; 194 | } 195 | 196 | @Override 197 | public boolean equals(Object obj) { 198 | if (this == obj) return true; 199 | if (obj == null || getClass() != obj.getClass()) return false; 200 | 201 | PlayerDataSnapshot that = (PlayerDataSnapshot) obj; 202 | 203 | if (Double.compare(that.health, health) != 0) return false; 204 | if (foodLevel != that.foodLevel) return false; 205 | if (Float.compare(that.saturation, saturation) != 0) return false; 206 | if (Double.compare(that.locX, locX) != 0) return false; 207 | if (Double.compare(that.locY, locY) != 0) return false; 208 | if (Double.compare(that.locZ, locZ) != 0) return false; 209 | if (Float.compare(that.totalXp, totalXp) != 0) return false; 210 | if (!worldName.equals(that.worldName)) return false; 211 | if (!Arrays.equals(finalMainInvContents, that.finalMainInvContents)) return false; 212 | if (!Arrays.equals(finalMainInvArmor, that.finalMainInvArmor)) return false; 213 | if (!Arrays.equals(finalEnderInvContents, that.finalEnderInvContents)) return false; 214 | 215 | return true; 216 | } 217 | 218 | @Override 219 | public String toString() { 220 | return "PlayerDataSnapshot{" + 221 | "totalXp=" + totalXp + 222 | ", health=" + health + 223 | ", foodLevel=" + foodLevel + 224 | ", saturation=" + saturation + 225 | ", worldName='" + worldName + '\'' + 226 | ", locX=" + locX + 227 | ", locY=" + locY + 228 | ", locZ=" + locZ + 229 | ", finalMainInvContents=" + Arrays.toString(finalMainInvContents) + 230 | ", finalMainInvArmor=" + Arrays.toString(finalMainInvArmor) + 231 | ", finalEnderInvContents=" + Arrays.toString(finalEnderInvContents) + 232 | '}'; 233 | } 234 | } 235 | 236 | //Conversion to Base64 code courtesy of github.com/JustRayz 237 | public static String toBase64(ItemStack[] contents) { 238 | return ItemStackSerialization.serialize(contents); 239 | } 240 | 241 | //Credits to Dev_Richard (https://www.spigotmc.org/members/dev_richard.38792/) 242 | //https://gist.github.com/RichardB122/8958201b54d90afbc6f0 243 | private float getTotalExperience(Player player) { 244 | int level = player.getLevel(); 245 | float currentExp = player.getExp(); 246 | int experience; 247 | int requiredExperience; 248 | 249 | if(level >= 0 && level <= 15) { 250 | experience = (int) Math.ceil(Math.pow(level, 2) + (6 * level)); 251 | requiredExperience = 2 * level + 7; 252 | } else if(level > 15 && level <= 30) { 253 | experience = (int) Math.ceil((2.5 * Math.pow(level, 2) - (40.5 * level) + 360)); 254 | requiredExperience = 5 * level - 38; 255 | } else { 256 | experience = (int) Math.ceil((4.5 * Math.pow(level, 2) - (162.5 * level) + 2220)); 257 | requiredExperience = 9 * level - 158; 258 | } 259 | 260 | experience += Math.ceil(currentExp * requiredExperience); 261 | 262 | return experience; 263 | } 264 | 265 | public static void cleanup(UUID uuid) { 266 | rateLimiters.remove(uuid); 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/reflections/LegacyNBTWrapper.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.reflections; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.nuclyon.technicallycoded.inventoryrollback.customdata.CustomDataItemEditor; 5 | import com.tcoded.lightlibs.bukkitversion.BukkitVersion; 6 | import me.danjono.inventoryrollback.config.ConfigData; 7 | import org.bukkit.inventory.ItemStack; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.lang.reflect.Method; 12 | import java.util.HashMap; 13 | import java.util.function.Consumer; 14 | 15 | public class LegacyNBTWrapper implements CustomDataItemEditor { 16 | 17 | private ItemStack item; 18 | private final NMSHandler nmsHandler; 19 | 20 | private static HashMap, String> getTagElementMethodNames; 21 | private static HashMap, String> setTagElementMethodNames; 22 | 23 | // 1.8 - 1.20.4 24 | private static String getTagMethodName; 25 | private static String setTagMethodName; 26 | 27 | private static Method getDataComponentMapMethod; 28 | 29 | private static Object customDataComponentMapKey; // custom_data key 30 | private static Method getDataComponentValueMethod; 31 | private static Method getCustomDataNBTCopyMethod; 32 | private static Method updateCustomDataNBTStaticMethod; 33 | 34 | public LegacyNBTWrapper(ItemStack item) { 35 | this.nmsHandler = new NMSHandler(); 36 | this.item = item; 37 | 38 | if (getTagElementMethodNames == null) { 39 | InventoryRollbackPlus irp = InventoryRollbackPlus.getInstance(); 40 | if (ConfigData.isDebugEnabled()) 41 | irp.getLogger().info("NBTWrapper created for the first time since startup!"); 42 | 43 | BukkitVersion nmsVersion = irp.getVersion(); 44 | if (ConfigData.isDebugEnabled()) irp.getLogger().info("Using NMS Version: " + nmsVersion.toString()); 45 | 46 | if (nmsVersion.greaterOrEqThan(BukkitVersion.v1_20_R4)) { 47 | resolve1_20_5OrHigherReflectionNames(nmsVersion); 48 | } else { 49 | resolvePre1_20_5ReflectionNames(nmsVersion); 50 | } 51 | 52 | resolveNbtTagCompoundReflectionNames(nmsVersion); 53 | } 54 | } 55 | 56 | private static void resolve1_20_5OrHigherReflectionNames(BukkitVersion nmsVersion) { 57 | try { 58 | // 1.20.5 or higher (1.20.5 now places custom NBT in a custom_data component) 59 | String getDataComponentMapMethodName = "a"; 60 | 61 | Class dataComponentsClass = Class.forName("net.minecraft.core.component.DataComponents"); 62 | customDataComponentMapKey = dataComponentsClass.getField("b").get(null); 63 | 64 | Class nmsItemStackClass = Class.forName("net.minecraft.world.item.ItemStack"); 65 | getDataComponentMapMethod = nmsItemStackClass.getMethod(getDataComponentMapMethodName); 66 | 67 | Class dataComponentMapClass = Class.forName("net.minecraft.core.component.DataComponentMap"); 68 | Class dataComponentTypeClass = Class.forName("net.minecraft.core.component.DataComponentType"); 69 | getDataComponentValueMethod = dataComponentMapClass.getMethod("a", dataComponentTypeClass); 70 | 71 | Class customDataClass = Class.forName("net.minecraft.world.item.component.CustomData"); 72 | Class itemStackClass = Class.forName("net.minecraft.world.item.ItemStack"); 73 | 74 | if (nmsVersion.greaterOrEqThan(BukkitVersion.v1_21_R3)) getCustomDataNBTCopyMethod = customDataClass.getMethod("d"); 75 | else getCustomDataNBTCopyMethod = customDataClass.getMethod("c"); 76 | 77 | updateCustomDataNBTStaticMethod = customDataClass.getMethod("a", dataComponentTypeClass, itemStackClass, Consumer.class); 78 | } catch (Exception ex) { 79 | ex.printStackTrace(); 80 | } 81 | } 82 | 83 | private static void resolvePre1_20_5ReflectionNames(BukkitVersion nmsVersion) { 84 | if (nmsVersion.greaterOrEqThan(BukkitVersion.v1_18_R1)) { 85 | 86 | if (nmsVersion.greaterOrEqThan(BukkitVersion.v1_20_R1)) { 87 | getTagMethodName = "v"; 88 | } 89 | else if (nmsVersion.greaterOrEqThan(BukkitVersion.v1_19_R1)) { 90 | getTagMethodName = "u"; 91 | } 92 | else if (nmsVersion.greaterOrEqThan(BukkitVersion.v1_18_R2)) { 93 | getTagMethodName = "t"; 94 | } 95 | else { 96 | getTagMethodName = "s"; 97 | } 98 | 99 | setTagMethodName = "c"; 100 | } else { 101 | getTagMethodName = "getTag"; 102 | setTagMethodName = "setTag"; 103 | } 104 | } 105 | 106 | private void resolveNbtTagCompoundReflectionNames(BukkitVersion nmsVersion) { 107 | getTagElementMethodNames = new HashMap<>(); 108 | setTagElementMethodNames = new HashMap<>(); 109 | 110 | if (nmsVersion.greaterOrEqThan(BukkitVersion.v1_18_R1)) { 111 | getTagElementMethodNames.put(Integer.class, "h"); 112 | getTagElementMethodNames.put(Long.class, "i"); 113 | getTagElementMethodNames.put(Float.class, "j"); 114 | getTagElementMethodNames.put(Double.class, "k"); 115 | getTagElementMethodNames.put(String.class, "l"); 116 | 117 | setTagElementMethodNames.put(Integer.class, "a"); 118 | setTagElementMethodNames.put(Long.class, "a"); 119 | setTagElementMethodNames.put(Float.class, "a"); 120 | setTagElementMethodNames.put(Double.class, "a"); 121 | setTagElementMethodNames.put(String.class, "a"); 122 | } else { 123 | getTagElementMethodNames.put(Integer.class, "getInt"); 124 | getTagElementMethodNames.put(Long.class, "getLong"); 125 | getTagElementMethodNames.put(Float.class, "getFloat"); 126 | getTagElementMethodNames.put(Double.class, "getDouble"); 127 | getTagElementMethodNames.put(String.class, "getString"); 128 | 129 | setTagElementMethodNames.put(Integer.class, "setInt"); 130 | setTagElementMethodNames.put(Long.class, "setLong"); 131 | setTagElementMethodNames.put(Float.class, "setFloat"); 132 | setTagElementMethodNames.put(Double.class, "setDouble"); 133 | setTagElementMethodNames.put(String.class, "setString"); 134 | } 135 | } 136 | 137 | public ItemStack setItemData() { 138 | return item; 139 | } 140 | 141 | public boolean hasUUID() { 142 | String uuid = getString("uuid"); 143 | 144 | return (uuid != null && !uuid.isEmpty()); 145 | } 146 | 147 | public ItemStack setString(String key, String data) { 148 | return writeDataToBukkitItem(key, String.class, data); 149 | } 150 | 151 | public ItemStack setInt(String key, Integer data) { 152 | return writeDataToBukkitItem(key, int.class, data); 153 | } 154 | 155 | public ItemStack setLong(String key, Long data) { 156 | return writeDataToBukkitItem(key, long.class, data); 157 | } 158 | 159 | public ItemStack setDouble(String key, Double data) { 160 | return writeDataToBukkitItem(key, double.class, data); 161 | } 162 | 163 | public ItemStack setFloat(String key, Float data) { 164 | return writeDataToBukkitItem(key, float.class, data); 165 | } 166 | 167 | public String getString(String key) { 168 | return readDataFromBukkitItem(key, String.class, String.class); 169 | } 170 | 171 | public int getInt(String key) { 172 | return readDataFromBukkitItem(key, int.class, Integer.class); 173 | } 174 | 175 | public Long getLong(String key) { 176 | return readDataFromBukkitItem(key, long.class, Long.class); 177 | } 178 | 179 | public double getDouble(String key) { 180 | return readDataFromBukkitItem(key, double.class, Double.class); 181 | } 182 | 183 | public Float getFloat(String key) { 184 | return readDataFromBukkitItem(key, float.class, Float.class); 185 | } 186 | 187 | private @Nullable T readDataFromBukkitItem(String key, Class dataType, Class mapType) { 188 | T result = null; 189 | 190 | try { 191 | Object itemstack = nmsHandler.getCraftBukkitClass("inventory.CraftItemStack") 192 | .getMethod("asNMSCopy", ItemStack.class) 193 | .invoke(null, item); 194 | 195 | if (InventoryRollbackPlus.getInstance().getVersion().greaterOrEqThan(BukkitVersion.v1_20_R4)) { 196 | result = readCustomDataFromNmsItem(itemstack, key, dataType, mapType); 197 | } else { 198 | result = readNbtFromNmsItem(itemstack, key, dataType, mapType); 199 | } 200 | } catch (Exception e) { 201 | e.printStackTrace(); 202 | } 203 | 204 | // noinspection ReassignedVariable 205 | return result; 206 | } 207 | 208 | private T readCustomDataFromNmsItem(Object nmsItem, String key, Class dataType, Class mapType) 209 | throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 210 | Object compMap = getDataComponentMapMethod.invoke(nmsItem); 211 | Object customData = getDataComponentValueMethod.invoke(compMap, customDataComponentMapKey); 212 | if (customData == null) return null; 213 | Object nbtComp = getCustomDataNBTCopyMethod.invoke(customData); 214 | return this.readNbtValue(key, mapType, nbtComp); 215 | } 216 | 217 | private T readNbtFromNmsItem(Object nmsItem, String key, Class dataType, Class mapType) 218 | throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 219 | Object comp = null; 220 | try { 221 | comp = nmsItem.getClass().getMethod(getTagMethodName).invoke(nmsItem); 222 | } catch (NullPointerException e) { 223 | return null; 224 | } 225 | 226 | return readNbtValue(key, mapType, comp); 227 | } 228 | 229 | private @Nullable T readNbtValue(String key, Class mapType, Object nbtComponent) 230 | throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 231 | try { 232 | // noinspection unchecked 233 | return (T) nbtComponent.getClass().getMethod(getTagElementMethodNames.get(mapType), String.class) 234 | .invoke(nbtComponent, key); 235 | } catch (NullPointerException e) { 236 | return null; 237 | } 238 | } 239 | 240 | private ItemStack writeDataToBukkitItem(String key, Class dataType, Object data) { 241 | try { 242 | Object nmsItem = nmsHandler.getCraftBukkitClass("inventory.CraftItemStack") 243 | .getMethod("asNMSCopy", ItemStack.class) 244 | .invoke(null, item); 245 | 246 | if (InventoryRollbackPlus.getInstance().getVersion().greaterOrEqThan(BukkitVersion.v1_20_R4)) { 247 | writeCustomDataToNmsItem(nmsItem, key, dataType, data); 248 | } else { 249 | writeNbtToNmsItem(nmsItem, key, dataType, data); 250 | } 251 | 252 | item = (ItemStack) nmsHandler.getCraftBukkitClass("inventory.CraftItemStack") 253 | .getMethod("asBukkitCopy", nmsItem.getClass()) 254 | .invoke(null, nmsItem); 255 | } catch (Exception e) { 256 | e.printStackTrace(); 257 | } 258 | 259 | return item; 260 | } 261 | 262 | private void writeCustomDataToNmsItem(Object nmsItem, String key, Class dataType, Object data) 263 | throws InvocationTargetException, IllegalAccessException { 264 | updateCustomDataNBTStaticMethod.invoke(null, customDataComponentMapKey, nmsItem, (Consumer) (comp) -> { 265 | try { 266 | writeNbtValue(comp, key, dataType, data); 267 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { 268 | ex.printStackTrace(); 269 | } 270 | }); 271 | } 272 | 273 | private void writeNbtToNmsItem(Object nmsItem, String key, Class dataType, Object data) 274 | throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, InstantiationException { 275 | Object comp = nmsItem.getClass().getMethod(getTagMethodName).invoke(nmsItem); 276 | 277 | if (comp == null) { 278 | if (InventoryRollbackPlus.getInstance().getVersion().greaterOrEqThan(BukkitVersion.v1_17_R1)) { 279 | comp = nmsHandler.getNMSClass("nbt.NBTTagCompound").newInstance(); 280 | } else { 281 | comp = nmsHandler.getNMSClass("NBTTagCompound").newInstance(); 282 | } 283 | } 284 | 285 | writeNbtValue(comp, key, dataType, data); 286 | 287 | nmsItem.getClass().getMethod(setTagMethodName, comp.getClass()).invoke(nmsItem, comp); 288 | } 289 | 290 | private static void writeNbtValue(Object nbtComponent, String key, Class dataType, Object data) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 291 | nbtComponent.getClass().getMethod(setTagElementMethodNames.get(data.getClass()), String.class, dataType) 292 | .invoke(nbtComponent, key, data); 293 | } 294 | 295 | } 296 | -------------------------------------------------------------------------------- /src/main/java/me/danjono/inventoryrollback/reflections/NMSHandler.java: -------------------------------------------------------------------------------- 1 | package me.danjono.inventoryrollback.reflections; 2 | 3 | import com.nuclyon.technicallycoded.inventoryrollback.InventoryRollbackPlus; 4 | import com.tcoded.lightlibs.bukkitversion.BukkitVersion; 5 | import me.danjono.inventoryrollback.InventoryRollback; 6 | import org.bukkit.Bukkit; 7 | 8 | public class NMSHandler { 9 | 10 | public Class getNMSClass(String name) { 11 | Class c = null; 12 | 13 | try { 14 | if (InventoryRollbackPlus.getInstance().getVersion().greaterOrEqThan(BukkitVersion.v1_17_R1)) { 15 | c = Class.forName("net.minecraft." + name); 16 | } else { 17 | c = Class.forName("net.minecraft.server." + InventoryRollback.getPackageVersion() + "." + name); 18 | } 19 | } catch (ClassNotFoundException e) { 20 | e.printStackTrace(); 21 | } 22 | 23 | return c; 24 | } 25 | 26 | public Class getCraftBukkitClass(String name) { 27 | Class c = null; 28 | 29 | try { 30 | c = Class.forName(Bukkit.getServer().getClass().getPackage().getName() + "." + name); 31 | } catch (ClassNotFoundException e) { 32 | e.printStackTrace(); 33 | } 34 | 35 | return c; 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/resources/.gitignore: -------------------------------------------------------------------------------- 1 | /changelog 2 | 3 | out/ 4 | target/ 5 | 6 | .idea/ 7 | .settings/ 8 | .git/ 9 | 10 | .classpath 11 | .project 12 | 13 | *.iml 14 | -------------------------------------------------------------------------------- /src/main/resources/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2021 TechnicallyCoded 2 | 3 | Definitions 4 | 1) I & TechnicallyCoded - both refer to the person who rightfully withheld 5 | ownership of the premium Mojang account under the Minecraft name 6 | "TechnicallyCoded", the 15th of November 2020 7 | 2) Project - refers to the entirety of code files and resources present in 8 | the github repository https://github.com/TechnicallyCoded/Inventory-Rollback-Plus 9 | OR any files and resources packaged into the InventoryRollbackPlus plugin I 10 | authored. 11 | 12 | This project is forked from https://github.com/danjono/Inventory-Rollback 13 | which is under the MIT LICENSE. I reserve all rights to any modifications I 14 | have made to this project's code which were NOT present in the repository 15 | this project was forked from before forking such repository. Any changes 16 | from the parent repository which are merged into this project in the future 17 | are also under the MIT LICENSE (at the time of writing). If the forked 18 | repository makes changes to the license, any new code from such repository, 19 | if applicable, is by default under the aforementioned new license. 20 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | ## Disable if you do not wish the plugin to run at all. 2 | enabled: true 3 | 4 | ## Maximum saves a backup will hold per type per user. 5 | max-saves: 6 | join: 10 7 | quit: 10 8 | death: 50 9 | world-change: 10 10 | force: 10 11 | 12 | ## Number of lines shown with backups on (Max: 5) 13 | backup-lines-visible: 4 14 | 15 | ## Set folder path where the data is saved to. Set as "DEFAULT" to keep it in the plugin folder. 16 | folder-location: 'DEFAULT' 17 | 18 | ## If enabled, this will disable YAML file backups in favour of MySQL storage. 19 | ## Requires a MySQL server. 20 | mysql: 21 | enabled: false 22 | details: 23 | host: 'localhost' 24 | port: 3306 25 | database: 'inventory_rollback' 26 | table-prefix: 'backup_' 27 | username: 'username' 28 | password: 'password' 29 | use-SSL: true 30 | verifyCertificate: true 31 | allowPubKeyRetrieval: false 32 | 33 | ## Sounds will play to the player when parts of their player is restored. 34 | sounds: 35 | teleport: 36 | enabled: true 37 | inventory: 38 | enabled: true 39 | food: 40 | enabled: true 41 | hunger: 42 | enabled: true 43 | xp: 44 | enabled: true 45 | 46 | ## Set how the time is displayed on the backups. 47 | ## Your time zone (EST, PST, GMT, ...) or a UTC offset (UTC+1, UTC-5, ...) can be used. 48 | time-zone: 'GMT' 49 | ## For more information on how to format the time, see: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html 50 | time-format: 'dd/MM/yyyy hh:mm:ss a z' 51 | 52 | ## Choose if we should wait until other plugins handle the player's death. 53 | ## If set to false, the plugin will handle the death event immediately. (before most plugins) 54 | ## If set to true, the plugin will wait for other plugins to handle the death event first. (after most plugins) 55 | ## -- 56 | ## NOTE #1: There are some plugins who also attempt to be the first to handle a player death. 57 | ## If so, these plugins may still overwrite the player's inventory before this plugin can save it. 58 | ## NOTE #2: If you rely on the ability to edit a player's inventory on death, you should set this to true. 59 | allow-other-plugins-edit-death-inventory: false 60 | 61 | ## Enabling this option will show a button to restore a player's backup fully to their character when online. 62 | ## This will overwrite their current inventory completely and wipe what they currently have without backing it up first. 63 | restore-to-player-button: true 64 | 65 | ## Check for updates on Spigot when the server is started. 66 | update-checker: true 67 | 68 | ## Allow bStats to collect anonymous server data. 69 | ## Please leave enabled as it helps us improve the plugin and provide better features. 70 | bStats: true 71 | 72 | # Debug mode - Prints extra information to the console. 73 | debug: false -------------------------------------------------------------------------------- /src/main/resources/lang/en_us.yml: -------------------------------------------------------------------------------- 1 | # Original messages by https://github.com/danjono & https://github.com/TechnicallyCoded 2 | general: 3 | prefix: '&f[&bInventoryRollbackPlus&f]&r ' 4 | 5 | commands: 6 | no-permission: '&cYou do not have permission!' 7 | error: '&cInvalid command.' 8 | enable: '&2The plugin has been enabled.' 9 | disable: '&2The plugin has been disabled.' 10 | reload: '&2The plugin has been reloaded successfully.' 11 | player-only: '&cCommand can only be run by a player.' 12 | import: '&aSuccessfully imported all backup data from the old plugin.' 13 | 14 | backup: 15 | no-backup: 'There is currently no backups for %NAME%.' 16 | not-online: '%NAME% is not currently online.' 17 | force-saved-player: '%NAME%''s inventory has been force saved.' 18 | force-saved-all: 'All online player inventories have been force saved.' 19 | not-forced-saved: 'There was an issue with saving %NAME%''s inventory.' 20 | 21 | attribute-restore: 22 | main-inventory: 23 | restored: '%NAME%''s main inventory has been restored.' 24 | restored-player: 'Your inventory has been restored by %NAME% from backup.' 25 | not-online: 'You can''t restore %NAME%''s inventory while they are offline.' 26 | button-name: '&cOverwrite Inventory from Backup' 27 | button-disabled: '&cSingle click restore is disabled\n&cin the config.yml file!' 28 | ender-chest: 29 | restored: '%NAME%''s ender chest has been restored.' 30 | restored-player: 'Your ender chest has been restored by %NAME% from backup.' 31 | not-online: 'You can''t restore %NAME%''s ender chest while they are offline.' 32 | button-name: '&dRestore Ender Chest' 33 | health: 34 | restored: '%NAME%''s health has been restored.' 35 | restored-player: 'Your health has been restored by %NAME% from backup.' 36 | not-online: 'You can''t restore %NAME%''s health while they are offline.' 37 | button-name: '&aRestore Health' 38 | hunger: 39 | restored: '%NAME%''s hunger has been restored.' 40 | restored-player: 'Your hunger has been restored by %NAME% from backup.' 41 | not-online: 'You can''t restore %NAME%''s hunger while they are offline.' 42 | button-name: '&cRestore Food' 43 | experience: 44 | restored: '%NAME%''s XP has been set to level %XP%.' 45 | restored-player: 'Your XP has been restored to level %XP% by %NAME% from backup.' 46 | not-online: 'You can''t restore %NAME%''s experience while they are offline.' 47 | button-name: '&2Restore Player XP' 48 | button-lore: '&rLevel %XP%' 49 | 50 | death-location: 51 | world: '&6World: &f%WORLD%' 52 | x: '&6X: &f%X%' 53 | y: '&6Y: &f%Y%' 54 | z: '&6Z: &f%Z%' 55 | reason: '&6Death reason: &f%REASON%' 56 | time: '&6Time: &f%TIME%' 57 | teleport-to: '&3Teleport to where this entry was logged.' 58 | teleport: 'You have been teleported to %LOCATION%' 59 | invalid-world: 'The world %WORLD% is not currently loaded on the server.' 60 | 61 | menu-buttons: 62 | main-menu: '&fMain Menu' 63 | next-page: '&fNext Page' 64 | previous-page: '&fPrevious Page' 65 | back-page: '&fBack' -------------------------------------------------------------------------------- /src/main/resources/lang/es_ar.yml: -------------------------------------------------------------------------------- 1 | # Translation by https://github.com/molocodev 2 | general: 3 | prefix: '&f[&bInventoryRollbackPlus&f]&r ' 4 | 5 | commands: 6 | no-permission: '&c¡No tenés permisos!' 7 | error: '&cComando inválido.' 8 | enable: '&2El plugin se habilitó.' 9 | disable: '&2El plugin se desactivó.' 10 | reload: '&2El plugin se recargó de forma exitosa.' 11 | player-only: '&cEl comando solo lo puede enviar un jugador.' 12 | # Translation missing, please contribute by creating an issue here 13 | # https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 14 | import: '&aSuccessfully imported all backup data from the old plugin.' 15 | 16 | backup: 17 | no-backup: 'Actualmente no hay backups para %NAME%.' 18 | not-online: '%NAME% no está conectado en este momento.' 19 | force-saved-player: 'El inventario de %NAME% fue guardado de forma forzosa.' 20 | force-saved-all: 'Se guardaron todos los inventarios de forma forsoza.' 21 | not-forced-saved: 'Hubo un problema al guardar el inventario de %NAME%.' 22 | 23 | attribute-restore: 24 | main-inventory: 25 | restored: 'Se restaruo el inventario principal de %NAME%.' 26 | restored-player: 'Tu inventario fue restablecido por %NAME% usando un backup.' 27 | not-online: 'No podés restablecer el inventario de %NAME% mientras que este offline.' 28 | button-name: '&dRestaurar el Inventario' 29 | # Translation missing, please contribute by creating an issue here 30 | # https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 31 | button-disabled: '&cSingle click restore is disabled\n&cin the config.yml file!' 32 | ender-chest: 33 | restored: 'Se restaruo el cofre de ender de %NAME%.' 34 | restored-player: 'Tu cofre de ender fue restablecido por %NAME% usando un backup.' 35 | not-online: 'No podés restablecer el cofre de ender de %NAME% mientras que este offline.' 36 | button-name: '&dRestaurar el Cofre de Ender' 37 | health: 38 | restored: 'La vida de %NAME% fue restablecida.' 39 | restored-player: 'Tu vida fue restablecido por %NAME% usando un backup.' 40 | not-online: 'No podés restablecer la vida de %NAME% mientras que este offline.' 41 | button-name: '&dRestaurar la Vida' 42 | hunger: 43 | restored: 'El hambre de %NAME% fue restablecido.' 44 | restored-player: 'Tu hambre fue restablecido por %NAME% usando un backup.' 45 | not-online: 'No podes restablecer el hambre de %NAME% mientras que este offline.' 46 | button-name: '&dRestaurar el Hambre' 47 | experience: 48 | restored: 'La experiencia de %NAME% fue restablecida al nivel %XP%.' 49 | restored-player: 'Tu experiencia fue restablecida al nivel %XP% por %NAME% usando un backup.' 50 | not-online: 'No podés restablecer la experiencia de %NAME% mientras que este desconectado.' 51 | button-name: '&2Restablecer XP del Jugador' 52 | button-lore: '&rNivel %XP%' 53 | 54 | death-location: 55 | world: '&6Mundo: &f%WORLD%' 56 | x: '&6X: &f%X%' 57 | y: '&6Y: &f%Y%' 58 | z: '&6Z: &f%Z%' 59 | reason: '&6Razon: &f%REASON%' 60 | time: '&6Hora: &f%TIME%' 61 | teleport-to: '&3Teletransportarse al lugar del registro.' 62 | teleport: 'Fuiste teletransportado a %LOCATION%' 63 | invalid-world: 'El mundo %WORLD% no está cargado en el servidor.' 64 | 65 | menu-buttons: 66 | main-menu: '&fMenu Principal' 67 | next-page: '&fPagina Siguiente' 68 | previous-page: '&fPagina Anterior' 69 | back-page: '&fAtras' -------------------------------------------------------------------------------- /src/main/resources/lang/es_es.yml: -------------------------------------------------------------------------------- 1 | # Translation by https://github.com/molocodev 2 | general: 3 | prefix: '&f[&bInventoryRollbackPlus&f]&r ' 4 | 5 | commands: 6 | no-permission: '&c¡No tienes permisos!' 7 | error: '&cComando inválido.' 8 | enable: '&2El plugin ha sido habilitado' 9 | disable: '&2El plugin ha sido deshabilitado.' 10 | reload: '&2El plugin ha sido recargado.' 11 | player-only: '&cEl comando solo lo puede enviar un jugador.' 12 | # Translation missing, please contribute by creating an issue here 13 | # https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 14 | import: '&aSuccessfully imported all backup data from the old plugin.' 15 | 16 | backup: 17 | no-backup: 'No existen backups para %NAME% en este momento.' 18 | not-online: '%NAME% no está conectado en este momento.' 19 | force-saved-player: 'El inventario de %NAME% ha sido guardado de forma forzosa.' 20 | force-saved-all: 'Todos los inventarios se han guardado de forma forsoza.' 21 | not-forced-saved: 'Ha habido un error al guardar el inventario de %NAME%.' 22 | 23 | attribute-restore: 24 | main-inventory: 25 | restored: 'Se restauró el inventario principal de %NAME%.' 26 | restored-player: 'Tu inventario ha sido restablecido por %NAME% utilizando un backup.' 27 | not-online: 'No puedes restablecer el inventario de %NAME% mientras que este desconectado.' 28 | button-name: '&dRestaurar el Inventario' 29 | # Translation missing, please contribute by creating an issue here https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 30 | button-disabled: '&cSingle click restore is disabled\n&cin the config.yml file!' 31 | ender-chest: 32 | restored: 'Se restauró el cofre de ender de %NAME%.' 33 | restored-player: 'Tu cofre de ender ha sido restablecido por %NAME% utilizando un backup.' 34 | not-online: 'No puedes restablecer el cofre de ender de %NAME% mientras que este desconectado.' 35 | button-name: '&dRestaurar el Cofre de Ender' 36 | health: 37 | restored: 'La vida de %NAME% fue restablecida.' 38 | restored-player: 'Tu vida ha sido restablecido por %NAME% utilizando un backup.' 39 | not-online: 'No puedes restablecer la vida de %NAME% mientras que este desconectado.' 40 | button-name: '&dRestaurar la Vida' 41 | hunger: 42 | restored: 'El hambre de %NAME% fue restablecido.' 43 | restored-player: 'Tu hambre ha sido restablecido por %NAME% utilizando un backup.' 44 | not-online: 'No puedes restablecer el hambre de %NAME% mientras que este desconectado.' 45 | button-name: '&dRestaurar el Hambre' 46 | experience: 47 | restored: 'La experiencia de %NAME% ha sido restablecida al nivel %XP%.' 48 | restored-player: 'Tu experiencia ha sido restablecida al nivel %XP% por %NAME% utilizando un backup.' 49 | not-online: 'No puedes restablecer la experiencia de %NAME% mientras que este desconectado.' 50 | button-name: '&2Restablecer XP del Jugador' 51 | button-lore: '&rNivel %XP%' 52 | 53 | death-location: 54 | world: '&6Mundo: &f%WORLD%' 55 | x: '&6X: &f%X%' 56 | y: '&6Y: &f%Y%' 57 | z: '&6Z: &f%Z%' 58 | reason: '&6Razon: &f%REASON%' 59 | time: '&6Hora: &f%TIME%' 60 | teleport-to: '&3Teletransportarse hacia el lugar del registro.' 61 | teleport: 'Has sido teletransportado a %LOCATION%' 62 | invalid-world: 'El mundo %WORLD% no se encuentra cargado en el servidor.' 63 | 64 | menu-buttons: 65 | main-menu: '&fMenu Principal' 66 | next-page: '&fPagina Siguiente' 67 | previous-page: '&fPagina Anterior' 68 | back-page: '&fAtras' -------------------------------------------------------------------------------- /src/main/resources/lang/fr_fr.yml: -------------------------------------------------------------------------------- 1 | # Translation by https://github.com/TechnicallyCoded 2 | general: 3 | prefix: '&f[&bInventoryRollbackPlus&f]&r ' 4 | 5 | commands: 6 | no-permission: '&cVous n''avez pas la permission!' 7 | error: '&cCommande invalide.' 8 | enable: '&2Le plugin a été activé.' 9 | disable: '&2Le plugin a été désactivé.' 10 | reload: '&2Le plugin a été rechargé correctement.' 11 | player-only: '&cLa commande peut seulement être éxecutée par un joueur.' 12 | # Translation missing, please contribute by creating an issue here 13 | # https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 14 | import: '&aLes données de l''ancien plugin ont été correctement importées.' 15 | 16 | backup: 17 | no-backup: 'Il n''y a aucune sauvegarde pour %NAME%.' 18 | not-online: '%NAME% n''est pas en ligne.' 19 | force-saved-player: 'L''inventaire de %NAME% a été sauvegardé manuellement.' 20 | force-saved-all: 'L''inventaires de tous des joueur en ligne ont été sauvegardés manuellement.' 21 | not-forced-saved: 'Une erreur s''est produite avec l''inventaire de %NAME%''s.' 22 | 23 | attribute-restore: 24 | main-inventory: 25 | restored: 'L''inventaire de %NAME% a été restauré.' 26 | restored-player: 'Votre inventaire a été restauré par %NAME% à partir d''une sauvegarde.' 27 | not-online: 'Vous ne pouvez pas restaurer l''inventaire de %NAME% pendant que ce joueur n''est pas en ligne.' 28 | button-name: '&dRestaurer l''Inventaire' 29 | button-disabled: '&cLa restauration en un clic est désactivée\n&cdans le fichier config.yml!' 30 | ender-chest: 31 | restored: 'L''ender chest de %NAME% a été restauré.' 32 | restored-player: 'Votre ender chest a été restauré par %NAME% à partir d''une sauvegarde.' 33 | not-online: 'Vous ne pouvez pas restaurer l''ender chest de %NAME% pendant que ce joueur n''est pas en ligne.' 34 | button-name: '&dRestaurer l''Ender Chest' 35 | health: 36 | restored: 'Les points de vie de %NAME% ont été restaurés.' 37 | restored-player: 'Vos points de vie ont été restaurés par %NAME% à partir d''une sauvegarde.' 38 | not-online: 'Vous ne pouvez pas restaurer les points de vie de %NAME% pendant que ce joueur n''est pas en ligne.' 39 | button-name: '&aRestaurer Les Points de Vie' 40 | hunger: 41 | restored: 'Les points de nourriture de %NAME% ont été restaurés.' 42 | restored-player: 'Vos points de nourriture ont été restaurés par %NAME% à partir d''une sauvegarde.' 43 | not-online: 'Vous ne pouvez pas restaurer les points de nourriture de %NAME% pendant que ce joueur n''est pas en ligne.' 44 | button-name: '&aRestaurer Les Points de Nourriture' 45 | experience: 46 | restored: 'L''XP de %NAME% a été restauré.' 47 | restored-player: 'Votre XP a été restauré par %NAME% à partir d''une sauvegarde.' 48 | not-online: 'Vous ne pouvez pas restaurer l''XP de %NAME% pendant que ce joueur n''est pas en ligne.' 49 | button-name: '&dRestaurer l''XP' 50 | 51 | death-location: 52 | world: '&6Monde: &f%WORLD%' 53 | x: '&6X: &f%X%' 54 | y: '&6Y: &f%Y%' 55 | z: '&6Z: &f%Z%' 56 | reason: '&6Raison de mort: &f%REASON%' 57 | time: '&6Heure: &f%TIME%' 58 | teleport-to: '&3Se téléporter à l''endroit où cette sauvegarde a été créée.' 59 | teleport: 'Vous avez été téléporté à %LOCATION%' 60 | invalid-world: 'Le monde %WORLD% n''existe pas actuellement sur ce serveur.' 61 | 62 | menu-buttons: 63 | main-menu: '&fMenu Principal' 64 | next-page: '&fProchaine Page' 65 | previous-page: '&fPage Précédente' 66 | back-page: '&fRetour' -------------------------------------------------------------------------------- /src/main/resources/lang/ru_ru.yml: -------------------------------------------------------------------------------- 1 | general: 2 | prefix: '&f[&bInventoryRollbackPlus&f]&r ' 3 | 4 | commands: 5 | no-permission: '&cУ вас нет разрешения!' 6 | error: '&cНеизвестная команда.' 7 | enable: '&2Плагин был включен.' 8 | disable: '&2Плагин был выключен.' 9 | reload: '&2Плагин был успешно перезагружен.' 10 | player-only: '&cКоманда может быть выполнена толшько игроком.' 11 | import: '&aУдачно импортированы все данные прошлой версии.' 12 | 13 | backup: 14 | no-backup: 'Нет сохранённых инвертарей для %NAME%.' 15 | not-online: '%NAME% в данный момент оффлайн.' 16 | force-saved-player: 'Инвертарь %NAME% был принудительно сохранён.' 17 | force-saved-all: 'Инвертари всех игроков онлайн были сохранены.' 18 | not-forced-saved: 'Возникли проблемы сохранения инвертаря %NAME%.' 19 | 20 | attribute-restore: 21 | main-inventory: 22 | restored: 'Тнвертарь %NAME% был восстановлен.' 23 | restored-player: 'Ваш инвертарь был восстановлен %NAME% из сохранения.' 24 | not-online: 'Вы не можете восстановить инвертарь %NAME% пока игрок оффлайн.' 25 | button-name: '&cПерезаписать инвертарь из сохранения' 26 | button-disabled: '&cВосстановление инвертаря по нажатию отключено в конфигурации плагина!' 27 | ender-chest: 28 | restored: 'Эндерчест %NAME% был восстановлен' 29 | restored-player: 'Ваш эндерчест был восстановлен %NAME% из сохранения.' 30 | not-online: 'Вы не можете восстановить эндерчест %NAME% пока игрок оффлайн.' 31 | button-name: '&dВосстановить эндерчест' 32 | health: 33 | restored: 'Здоровье %NAME% было восстановлено.' 34 | restored-player: 'Ваше здоровье было восстановлено %NAME% из сохранения.' 35 | not-online: 'Вы не можете восстановить здоровье %NAME% пока игрок оффлайн.' 36 | button-name: '&aВосстановить здоровье' 37 | hunger: 38 | restored: 'Сытость %NAME% была восстановлена.' 39 | restored-player: 'Ваша сытость была восстановлена %NAME% из сохранения.' 40 | not-online: 'Вы не можете восстановить сытость %NAME% пока игрок оффлайн.' 41 | button-name: '&cВосстановить сытость' 42 | experience: 43 | restored: 'Опыт %NAME% был установлен к %XP%.' 44 | restored-player: '%NAME% установил ваш опыт на %XP% из сохранения.' 45 | not-online: 'Вы не можете восстановить опыт %NAME% пока он оффлайн.' 46 | button-name: '&2Восстановить опыт игрока' 47 | button-lore: '&rУровень опыта %XP%' 48 | 49 | death-location: 50 | world: '&6Мир: &f%WORLD%' 51 | x: '&6X: &f%X%' 52 | y: '&6Y: &f%Y%' 53 | z: '&6Z: &f%Z%' 54 | reason: '&6Причина смерти: &f%REASON%' 55 | time: '&6Время: &f%TIME%' 56 | teleport-to: '&3Телепортироваться к месту сохранения.' 57 | teleport: 'Вы были телепортированы на %LOCATION%' 58 | invalid-world: 'Мир %WORLD% пока не загружен на сервере.' 59 | 60 | menu-buttons: 61 | main-menu: '&fГлавное меню' 62 | next-page: '&fСледующая страница' 63 | previous-page: '&fПредыдущая страница' 64 | back-page: '&fНазад' -------------------------------------------------------------------------------- /src/main/resources/lang/zh_cn.yml: -------------------------------------------------------------------------------- 1 | # Translation by https://github.com/Youwenqwq 2 | general: 3 | prefix: '&f[&bInventoryRollbackPlus&f]&r ' 4 | 5 | commands: 6 | no-permission: '&c你没有权限!' 7 | error: '&c无效指令' 8 | enable: '&2已启用插件' 9 | disable: '&2已关闭插件' 10 | reload: '&2插件已重载' 11 | player-only: '&c只能由玩家运行这个指令' 12 | # Translation missing, please contribute by creating an issue here 13 | # https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 14 | import: '&a成功从旧版插件中导入备份数据' 15 | 16 | backup: 17 | no-backup: '目前没有 %NAME% 的备份' 18 | not-online: '%NAME% 现在不在线' 19 | force-saved-player: '%NAME% 的背包已被保存' 20 | # Translation missing, please contribute by creating an issue here https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 21 | force-saved-all: '所有在线玩家的背包已被强制保存' 22 | not-forced-saved: '在保存 %NAME% 的背包时出现问题' 23 | 24 | attribute-restore: 25 | main-inventory: 26 | restored: '%NAME% 的背包已被回档.' 27 | restored-player: '你的背包已被 %NAME% 从备份中回档.' 28 | not-online: '%NAME% 不在线,因此背包无法回档.' 29 | button-name: '&d回档背包' 30 | # Translation missing, please contribute by creating an issue here https://github.com/TechnicallyCoded/Inventory-Rollback-Plus/issues/new/choose 31 | button-disabled: '&c单击回档已被禁用\n&c文件 config.yml!' 32 | ender-chest: 33 | restored: '%NAME% 的末影箱已被回档.' 34 | restored-player: '你的末影箱已被 %NAME% 从备份中回档.' 35 | not-online: '%NAME% 不在线,因此末影箱无法回档.' 36 | button-name: '&d回档末影箱' 37 | health: 38 | restored: '%NAME% 的生命值已被回档.' 39 | restored-player: '你的生命值已被 %NAME% 从备份中回档.' 40 | not-online: '%NAME% 不在线,因此生命值无法回档.' 41 | button-name: '&a回档生命值' 42 | hunger: 43 | restored: '%NAME% 的饱食度已被回档.' 44 | restored-player: '你的饱食度已被 %NAME% 从备份中回档.' 45 | not-online: '%NAME% 不在线,因此饱食度无法回档.' 46 | button-name: '&c回档饱食度' 47 | experience: 48 | restored: '%NAME% 的经验等级已被回档至 %XP%.' 49 | restored-player: '你的经验已被 %NAME% 从备份中回档至 %XP%.' 50 | not-online: '%NAME% 不在线,因此经验无法回档.' 51 | button-name: '&2回档玩家经验' 52 | button-lore: '&r经验等级 %XP%' 53 | 54 | death-location: 55 | world: '&6世界: &f%WORLD%' 56 | x: '&6X: &f%X%' 57 | y: '&6Y: &f%Y%' 58 | z: '&6Z: &f%Z%' 59 | reason: '&6死因: &f%REASON%' 60 | time: '&6时间: &f%TIME%' 61 | teleport-to: '&3传送到玩家死亡的地方.' 62 | teleport: '你传送到了 %LOCATION%' 63 | invalid-world: '世界 %WORLD% 现在并没有在服务器上加载.' 64 | 65 | menu-buttons: 66 | main-menu: '&f主菜单' 67 | next-page: '&f下一页' 68 | previous-page: '&f上一页' 69 | back-page: '&f返回' 70 | -------------------------------------------------------------------------------- /src/main/resources/messages.yml: -------------------------------------------------------------------------------- 1 | general: 2 | prefix: '&f[&bInventoryRollbackPlus&f]&r ' 3 | 4 | commands: 5 | no-permission: '&cYou do not have permission!' 6 | error: '&cInvalid command.' 7 | enable: '&2The plugin has been enabled.' 8 | disable: '&2The plugin has been disabled.' 9 | reload: '&2The plugin has been reloaded successfully.' 10 | player-only: '&cCommand can only be run by a player.' 11 | import: '&aSuccessfully imported all backup data from the old plugin.' 12 | 13 | backup: 14 | no-backup: 'There is currently no backups for %NAME%.' 15 | not-online: '%NAME% is not currently online.' 16 | force-saved-player: '%NAME%''s inventory has been force saved.' 17 | force-saved-all: 'All online player inventories have been force saved.' 18 | not-forced-saved: 'There was an issue with saving %NAME%''s inventory.' 19 | 20 | attribute-restore: 21 | main-inventory: 22 | restored: '%NAME%''s main inventory has been restored.' 23 | restored-player: 'Your inventory has been restored by %NAME% from backup.' 24 | not-online: 'You can''t restore %NAME%''s inventory while they are offline.' 25 | button-name: '&cOVERWRITE Inventory from Backup' 26 | button-disabled: '&cSingle click restore is disabled\n&cin the config.yml file!' 27 | ender-chest: 28 | restored: '%NAME%''s ender chest has been restored.' 29 | restored-player: 'Your ender chest has been restored by %NAME% from backup.' 30 | not-online: 'You can''t restore %NAME%''s ender chest while they are offline.' 31 | button-name: '&dRestore Ender Chest' 32 | health: 33 | restored: '%NAME%''s health has been restored.' 34 | restored-player: 'Your health has been restored by %NAME% from backup.' 35 | not-online: 'You can''t restore %NAME%''s health while they are offline.' 36 | button-name: '&aRestore Health' 37 | hunger: 38 | restored: '%NAME%''s hunger has been restored.' 39 | restored-player: 'Your hunger has been restored by %NAME% from backup.' 40 | not-online: 'You can''t restore %NAME%''s hunger while they are offline.' 41 | button-name: '&cRestore Food' 42 | experience: 43 | restored: '%NAME%''s XP has been set to level %XP%.' 44 | restored-player: 'Your XP has been restored to level %XP% by %NAME% from backup.' 45 | not-online: 'You can''t restore %NAME%''s experience while they are offline.' 46 | button-name: '&2Restore Player XP' 47 | button-lore: '&rLevel %XP%' 48 | 49 | death-location: 50 | world: '&6World: &f%WORLD%' 51 | x: '&6X: &f%X%' 52 | y: '&6Y: &f%Y%' 53 | z: '&6Z: &f%Z%' 54 | reason: '&6Death reason: &f%REASON%' 55 | time: '&6Time: &f%TIME%' 56 | teleport-to: '&3Teleport to where this entry was logged.' 57 | teleport: 'You have been teleported to %LOCATION%' 58 | invalid-world: 'The world %WORLD% is not currently loaded on the server.' 59 | 60 | menu-buttons: 61 | main-menu: '&fMain Menu' 62 | next-page: '&fNext Page' 63 | previous-page: '&fPrevious Page' 64 | back-page: '&fBack' -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ${project.name} 2 | main: ${project.groupId}.InventoryRollbackPlus 3 | version: ${project.version} 4 | description: ${project.description} 5 | author: ${project.organization.name} 6 | authors: [${project.organization.name}, danjono] 7 | api-version: 1.13 8 | 9 | loadbefore: 10 | - DeluxeCombat 11 | - DeadChest 12 | - AngelChest 13 | - SavageDeathChest 14 | 15 | commands: 16 | inventoryrollbackplus: 17 | description: ${project.description} 18 | usage: /irp 19 | aliases: [ir, irp, inventoryrollback] 20 | 21 | permissions: 22 | inventoryrollbackplus.*: 23 | description: Gives access to all InventoryRollbackPlus commands. 24 | children: 25 | inventoryrollbackplus.cmd: true 26 | inventoryrollbackplus.deathsave: true 27 | inventoryrollbackplus.joinsave: true 28 | inventoryrollbackplus.leavesave: true 29 | inventoryrollbackplus.worldchangesave: true 30 | inventoryrollbackplus.enable: true 31 | inventoryrollbackplus.disable: true 32 | inventoryrollbackplus.reload: true 33 | inventoryrollbackplus.restore: true 34 | inventoryrollbackplus.forcebackup: true 35 | inventoryrollbackplus.version: true 36 | inventoryrollbackplus.help: true 37 | inventoryrollbackplus.adminalerts: true 38 | inventoryrollbackplus.viewbackups: true 39 | default: op 40 | inventoryrollbackplus.cmd: 41 | description: Allows InventoryRollback commands to work. 42 | default: true 43 | inventoryrollbackplus.deathsave: 44 | description: Player inventories will be saved when they die. 45 | default: true 46 | inventoryrollbackplus.joinsave: 47 | description: Player inventories will be saved when the join. 48 | default: true 49 | inventoryrollbackplus.leavesave: 50 | description: Player inventories will be saved when they leave. 51 | default: true 52 | inventoryrollbackplus.worldchangesave: 53 | description: Player inventories will be saved when they change worlds. 54 | default: true 55 | inventoryrollbackplus.enable: 56 | description: Grants access to enable the plugin globally. 57 | default: op 58 | inventoryrollbackplus.disable: 59 | description: Grants access to disable the plugin globally. 60 | default: op 61 | inventoryrollbackplus.reload: 62 | description: Grants access to reload the plugin configuration. 63 | default: op 64 | inventoryrollbackplus.restore: 65 | description: Grants access to perform player roll backs. 66 | default: op 67 | children: 68 | inventoryrollbackplus.viewbackups: true 69 | inventoryrollbackplus.restore.teleport: 70 | default: op 71 | description: Allow the staff member to teleport to the location where the backup was saved 72 | inventoryrollbackplus.forcebackup: 73 | description: Forces a backup for an online player. 74 | default: op 75 | inventoryrollbackplus.viewbackups: 76 | description: Allows player to view player backups but can't take items from the backups or restore the target's inventory 77 | default: op 78 | inventoryrollbackplus.adminalerts: 79 | description: Allows player to receive extra notifications related to the plugin when joining. 80 | default: op 81 | inventoryrollbackplus.version: 82 | description: Allows player to see the version the plugin is running on the server. 83 | default: true 84 | inventoryrollbackplus.help: 85 | description: Get the list of commands and what they do 86 | default: true 87 | 88 | # ---- LEGACY COMPATIBILITY ---- 89 | inventoryrollback.cmd: 90 | description: Allows InventoryRollback commands to work. 91 | default: false 92 | children: 93 | inventoryrollbackplus.cmd: true 94 | inventoryrollback.deathsave: 95 | description: Player inventories will be saved when they die. 96 | default: false 97 | children: 98 | inventoryrollbackplus.deathsave: true 99 | inventoryrollback.joinsave: 100 | description: Player inventories will be saved when the join. 101 | default: false 102 | children: 103 | inventoryrollbackplus.joinsave: true 104 | inventoryrollback.leavesave: 105 | description: Player inventories will be saved when they leave. 106 | default: false 107 | children: 108 | inventoryrollbackplus.leavesave: true 109 | inventoryrollback.worldchangesave: 110 | description: Player inventories will be saved when they change worlds. 111 | default: false 112 | children: 113 | inventoryrollbackplus.worldchangesave: true 114 | inventoryrollback.enable: 115 | description: Grants access to enable the plugin globally. 116 | default: false 117 | children: 118 | inventoryrollbackplus.enable: true 119 | inventoryrollback.disable: 120 | description: Grants access to disable the plugin globally. 121 | default: false 122 | children: 123 | inventoryrollbackplus.disable: true 124 | inventoryrollback.reload: 125 | description: Grants access to reload the plugin configuration. 126 | default: false 127 | children: 128 | inventoryrollbackplus.reload: true 129 | inventoryrollback.restore: 130 | description: Grants access to perform player roll backs. 131 | default: false 132 | children: 133 | inventoryrollbackplus.restore: true 134 | inventoryrollback.forcebackup: 135 | description: Forces a backup for an online player. 136 | default: false 137 | children: 138 | inventoryrollbackplus.forcebackup: true 139 | inventoryrollback.version: 140 | description: Allows player to see the version the plugin is running on the server. 141 | default: false 142 | children: 143 | inventoryrollbackplus.version: true -------------------------------------------------------------------------------- /src/test/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/Version2SerializationIntTest.java: -------------------------------------------------------------------------------- 1 | // File: src/test/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/Version2SerializationIntTest.java 2 | 3 | package com.nuclyon.technicallycoded.inventoryrollback.util.serialization; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.lang.reflect.Method; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | 15 | public class Version2SerializationIntTest { 16 | 17 | private void invokeWriteInt(OutputStream os, int value) throws Exception { 18 | Method writeInt = Version2Serialization.class.getDeclaredMethod("writeInt", OutputStream.class, int.class); 19 | writeInt.setAccessible(true); 20 | writeInt.invoke(null, os, value); 21 | } 22 | 23 | private int invokeReadInt(InputStream is) throws Exception { 24 | Method readInt = Version2Serialization.class.getDeclaredMethod("readInt", InputStream.class); 25 | readInt.setAccessible(true); 26 | Object result = readInt.invoke(null, is); 27 | return (Integer) result; 28 | } 29 | 30 | @Test 31 | public void testWriteReadIntZero() throws Exception { 32 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 33 | int expected = 0; 34 | invokeWriteInt(baos, expected); 35 | ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 36 | int actual = invokeReadInt(bais); 37 | assertEquals(expected, actual, "Value 0 should be read correctly"); 38 | } 39 | 40 | @Test 41 | public void testWriteReadIntPositive() throws Exception { 42 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 43 | int expected = 123456789; 44 | invokeWriteInt(baos, expected); 45 | ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 46 | int actual = invokeReadInt(bais); 47 | assertEquals(expected, actual, "Positive value should be read correctly"); 48 | } 49 | 50 | @Test 51 | public void testWriteReadIntNegative() throws Exception { 52 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 53 | int expected = -987654321; 54 | invokeWriteInt(baos, expected); 55 | ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 56 | int actual = invokeReadInt(bais); 57 | assertEquals(expected, actual, "Negative value should be read correctly"); 58 | } 59 | 60 | @Test 61 | public void testWriteReadIntMaxValue() throws Exception { 62 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 63 | int expected = Integer.MAX_VALUE; 64 | invokeWriteInt(baos, expected); 65 | ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 66 | int actual = invokeReadInt(bais); 67 | assertEquals(expected, actual, "Integer.MAX_VALUE should be read correctly"); 68 | } 69 | 70 | @Test 71 | public void testWriteReadIntMinValue() throws Exception { 72 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 73 | int expected = Integer.MIN_VALUE; 74 | invokeWriteInt(baos, expected); 75 | ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 76 | int actual = invokeReadInt(bais); 77 | assertEquals(expected, actual, "Integer.MIN_VALUE should be read correctly"); 78 | } 79 | } -------------------------------------------------------------------------------- /src/test/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/Version2SerializationTest.java: -------------------------------------------------------------------------------- 1 | // File: src/test/java/com/nuclyon/technicallycoded/inventoryrollback/util/serialization/Version2SerializationTest.java 2 | 3 | package com.nuclyon.technicallycoded.inventoryrollback.util.serialization; 4 | 5 | import org.bukkit.inventory.ItemStack; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | public class Version2SerializationTest { 11 | 12 | @Test 13 | public void testSerializeEmptyArray() { 14 | ItemStack[] items = new ItemStack[0]; 15 | String serialized = Version2Serialization.serialize(items); 16 | assertNotNull(serialized, "Serialized data should not be null"); 17 | 18 | DeserializationResult result = Version2Serialization.deserialize(serialized); 19 | assertNull(result.getErrorMessage(), "There should be no error message during deserialization"); 20 | assertNotNull(result.getItems(), "The deserialized array should not be null"); 21 | assertEquals(0, result.getItems().length, "The length of the deserialized array should be 0"); 22 | } 23 | 24 | @Test 25 | public void testSerializeArrayWithNullItems() { 26 | ItemStack[] items = new ItemStack[] { null, null }; 27 | String serialized = Version2Serialization.serialize(items); 28 | assertNotNull(serialized, "Serialized data should not be null"); 29 | 30 | DeserializationResult result = Version2Serialization.deserialize(serialized); 31 | assertNull(result.getErrorMessage(), "There should be no error message during deserialization"); 32 | assertNotNull(result.getItems(), "The deserialized array should not be null"); 33 | assertEquals(2, result.getItems().length, "The deserialized array should have length 2"); 34 | assertNull(result.getItems()[0], "First item should be null"); 35 | assertNull(result.getItems()[1], "Second item should be null"); 36 | } 37 | 38 | @Test 39 | public void testDeserializeCorruptedData() { 40 | // Create invalid Base64 data to generate an error during deserialization 41 | String corruptedData = "not_base64_encoded_data"; 42 | DeserializationResult result = Version2Serialization.deserialize(corruptedData); 43 | assertNotNull(result.getErrorMessage(), "An error message is expected for corrupted data"); 44 | assertNull(result.getItems(), "ItemStacks array should be null when deserialization fails"); 45 | } 46 | } --------------------------------------------------------------------------------