55import org .bukkit .configuration .file .FileConfiguration ;
66import org .bukkit .configuration .file .YamlConfiguration ;
77
8- import java .io .File ;
8+ import java .io .*;
9+ import java .nio .charset .StandardCharsets ;
910import java .nio .file .Files ;
1011import java .nio .file .StandardCopyOption ;
1112import java .util .*;
@@ -24,4 +25,194 @@ public GuiLayoutUpdater(SmartSpawner plugin) {
2425 this .plugin = plugin ;
2526 this .currentVersion = plugin .getDescription ().getVersion ();
2627 }
28+
29+ /**
30+ * Check if GUI layouts need to be updated and update them if necessary
31+ */
32+ public void checkAndUpdateLayouts () {
33+ File layoutsDir = new File (plugin .getDataFolder (), GUI_LAYOUTS_DIR );
34+
35+ // Ensure layouts directory exists
36+ if (!layoutsDir .exists ()) {
37+ layoutsDir .mkdirs ();
38+ }
39+
40+ // Check and update each layout
41+ for (String layoutName : LAYOUT_NAMES ) {
42+ File layoutDir = new File (layoutsDir , layoutName );
43+ if (!layoutDir .exists ()) {
44+ layoutDir .mkdirs ();
45+ }
46+
47+ // Check and update each layout file
48+ for (String fileName : LAYOUT_FILES ) {
49+ checkAndUpdateLayoutFile (layoutDir , layoutName , fileName );
50+ }
51+ }
52+ }
53+
54+ /**
55+ * Check and update a specific layout file
56+ */
57+ private void checkAndUpdateLayoutFile (File layoutDir , String layoutName , String fileName ) {
58+ File layoutFile = new File (layoutDir , fileName );
59+
60+ // If layout file doesn't exist, create it with the version header
61+ if (!layoutFile .exists ()) {
62+ createDefaultLayoutWithHeader (layoutDir , layoutName , fileName );
63+ return ;
64+ }
65+
66+ FileConfiguration currentLayout = YamlConfiguration .loadConfiguration (layoutFile );
67+ String layoutVersionStr = currentLayout .getString (GUI_LAYOUT_VERSION_KEY , "0.0.0" );
68+ Version layoutVersion = new Version (layoutVersionStr );
69+ Version pluginVersion = new Version (currentVersion );
70+
71+ if (layoutVersion .compareTo (pluginVersion ) >= 0 ) {
72+ return ;
73+ }
74+
75+ plugin .getLogger ().info ("Updating GUI layout " + layoutName + "/" + fileName + " from version " + layoutVersionStr + " to " + currentVersion );
76+
77+ try {
78+ Map <String , Object > userValues = flattenConfig (currentLayout );
79+
80+ // Create temp file with new default layout
81+ File tempFile = new File (layoutDir , fileName + ".new" );
82+ createDefaultLayoutWithHeader (layoutDir , layoutName , fileName , tempFile );
83+
84+ FileConfiguration newLayout = YamlConfiguration .loadConfiguration (tempFile );
85+ newLayout .set (GUI_LAYOUT_VERSION_KEY , currentVersion );
86+
87+ // Check if there are actual differences before creating backup
88+ boolean layoutDiffers = hasLayoutDifferences (userValues , newLayout );
89+
90+ if (layoutDiffers ) {
91+ File backupFile = new File (layoutDir , fileName .replace (".yml" , "_backup_" + layoutVersionStr + ".yml" ));
92+ Files .copy (layoutFile .toPath (), backupFile .toPath (), StandardCopyOption .REPLACE_EXISTING );
93+ plugin .getLogger ().info ("GUI layout backup created at " + layoutName + "/" + backupFile .getName ());
94+ } else {
95+ plugin .debug ("No significant GUI layout changes detected for " + layoutName + "/" + fileName + ", skipping backup creation" );
96+ }
97+
98+ // Apply user values and save
99+ applyUserValues (newLayout , userValues );
100+ newLayout .save (layoutFile );
101+ tempFile .delete ();
102+
103+ } catch (Exception e ) {
104+ plugin .getLogger ().log (Level .SEVERE , "Failed to update GUI layout " + layoutName + "/" + fileName + ": " + e .getMessage (), e );
105+ }
106+ }
107+
108+ /**
109+ * Determines if there are actual differences between old and new layout
110+ */
111+ private boolean hasLayoutDifferences (Map <String , Object > userValues , FileConfiguration newLayout ) {
112+ // Get all paths from new layout (excluding gui_layout_version)
113+ Map <String , Object > newLayoutMap = flattenConfig (newLayout );
114+
115+ // Check for removed or changed keys
116+ for (Map .Entry <String , Object > entry : userValues .entrySet ()) {
117+ String path = entry .getKey ();
118+ Object oldValue = entry .getValue ();
119+
120+ // Skip gui_layout_version key
121+ if (path .equals (GUI_LAYOUT_VERSION_KEY )) continue ;
122+
123+ // Check if path no longer exists
124+ if (!newLayout .contains (path )) {
125+ return true ; // Found a removed path
126+ }
127+
128+ // Check if default value changed
129+ Object newDefaultValue = newLayout .get (path );
130+ if (newDefaultValue != null && !newDefaultValue .equals (oldValue )) {
131+ return true ; // Default value changed
132+ }
133+ }
134+
135+ // Check for new keys
136+ for (String path : newLayoutMap .keySet ()) {
137+ if (!path .equals (GUI_LAYOUT_VERSION_KEY ) && !userValues .containsKey (path )) {
138+ return true ; // Found a new path
139+ }
140+ }
141+
142+ return false ; // No significant differences
143+ }
144+
145+ /**
146+ * Create a default layout file with version header
147+ */
148+ private void createDefaultLayoutWithHeader (File layoutDir , String layoutName , String fileName ) {
149+ createDefaultLayoutWithHeader (layoutDir , layoutName , fileName , new File (layoutDir , fileName ));
150+ }
151+
152+ /**
153+ * Create a default layout file with version header at specific destination
154+ */
155+ private void createDefaultLayoutWithHeader (File layoutDir , String layoutName , String fileName , File destinationFile ) {
156+ try {
157+ // Ensure parent directory exists
158+ File parentDir = destinationFile .getParentFile ();
159+ if (parentDir != null && !parentDir .exists ()) {
160+ parentDir .mkdirs ();
161+ }
162+
163+ String resourcePath = GUI_LAYOUTS_DIR + "/" + layoutName + "/" + fileName ;
164+ try (InputStream in = plugin .getResource (resourcePath )) {
165+ if (in != null ) {
166+ List <String > defaultLines = new BufferedReader (new InputStreamReader (in , StandardCharsets .UTF_8 ))
167+ .lines ()
168+ .toList ();
169+
170+ List <String > newLines = new ArrayList <>();
171+ newLines .add ("# GUI Layout version - Do not modify this value" );
172+ newLines .add (GUI_LAYOUT_VERSION_KEY + ": " + currentVersion );
173+ newLines .add ("" );
174+ newLines .addAll (defaultLines );
175+
176+ Files .write (destinationFile .toPath (), newLines , StandardCharsets .UTF_8 );
177+ } else {
178+ plugin .getLogger ().warning ("Default GUI layout " + resourcePath + " not found in the plugin's resources." );
179+ destinationFile .createNewFile ();
180+ }
181+ }
182+ } catch (IOException e ) {
183+ plugin .getLogger ().log (Level .SEVERE , "Failed to create default GUI layout with header for " + layoutName + "/" + fileName + ": " + e .getMessage (), e );
184+ }
185+ }
186+
187+ /**
188+ * Flattens a configuration section into a map of path -> value
189+ */
190+ private Map <String , Object > flattenConfig (ConfigurationSection config ) {
191+ Map <String , Object > result = new HashMap <>();
192+ for (String key : config .getKeys (true )) {
193+ if (!config .isConfigurationSection (key )) {
194+ result .put (key , config .get (key ));
195+ }
196+ }
197+ return result ;
198+ }
199+
200+ /**
201+ * Applies the user values to the new layout
202+ */
203+ private void applyUserValues (FileConfiguration newLayout , Map <String , Object > userValues ) {
204+ for (Map .Entry <String , Object > entry : userValues .entrySet ()) {
205+ String path = entry .getKey ();
206+ Object value = entry .getValue ();
207+
208+ // Don't override gui_layout_version
209+ if (path .equals (GUI_LAYOUT_VERSION_KEY )) continue ;
210+
211+ if (newLayout .contains (path )) {
212+ newLayout .set (path , value );
213+ } else {
214+ plugin .getLogger ().warning ("GUI layout path '" + path + "' from old layout no longer exists in new layout" );
215+ }
216+ }
217+ }
27218}
0 commit comments