001package squidpony.store.text; 002 003import com.badlogic.gdx.Gdx; 004import com.badlogic.gdx.Preferences; 005import squidpony.Converters; 006import squidpony.LZSPlus; 007import squidpony.StringConvert; 008import squidpony.squidmath.OrderedMap; 009import squidpony.Garbler; 010 011import java.util.Map; 012 013/** 014 * Helps games store information in libGDX's Preferences class as Strings, then get it back out. Does not use JSON, 015 * instead using a customized and customizable manual serialization style based around {@link StringConvert}. 016 * Created by Tommy Ettinger on 9/16/2016. 017 */ 018public class TextStorage { 019 public final Preferences preferences; 020 public final String storageName; 021 protected OrderedMap<String, String> contents; 022 public final StringConvert<OrderedMap<String, String>> mapConverter; 023 public boolean compress = true; 024 public long[] garbleKey; 025 026 /** 027 * Please don't use this constructor if possible; it simply calls {@link #TextStorage(String)} with the constant 028 * String "nameless". This could easily overlap with other files/sections in Preferences, so you should always 029 * prefer giving a String argument to the constructor, typically the name of the game. 030 * @see #TextStorage(String) the recommended constructor to use 031 */ 032 public TextStorage() 033 { 034 this("nameless"); 035 } 036 037 /** 038 * Creates a JsonStorage with the given fileName to save using Preferences from libGDX. The name should generally 039 * be the name of this game or application, and must be a valid name for a file (so no slashes, backslashes, colons, 040 * semicolons, or commas for certain, and other non-alphanumeric characters are also probably invalid). You should 041 * not assume anything is present in the Preferences storage unless you have put it there, and this applies doubly 042 * to games or applications other than your own; you should avoid values for fileName that might overlap with 043 * another game's Preferences values. 044 * <br> 045 * To organize saved data into sub-sections, you specify logical units (like different players' saved games) with a 046 * String outerName when you call {@link #store(String)}, and can further distinguish data under the outerName when 047 * you call {@link #put(String, Object, StringConvert)} to put each individual item into the saved storage with its 048 * own innerName. 049 * <br> 050 * Calling this also sets up custom serializers for several important types in SquidLib; char[][], OrderedMap, 051 * IntDoubleOrderedMap, FakeLanguageGen, GreasedRegion, and notably Pattern from RegExodus all have smaller 052 * serialized representations than the default. OrderedMap allows non-String keys, which gets around a limitation in 053 * JSON maps normally, and both FakeLanguageGen and Pattern are amazingly smaller with the custom representation. 054 * The custom char[][] representation is about half the normal size by omitting commas after each char. 055 * @param fileName the valid file name to create or open from Preferences; typically the name of the game/app. 056 */ 057 public TextStorage(final String fileName) 058 { 059 this(fileName, new long[0]); 060 } 061 062 /** 063 * Creates a JsonStorage with the given fileName to save using Preferences from libGDX. The name should generally 064 * be the name of this game or application, and must be a valid name for a file (so no slashes, backslashes, colons, 065 * semicolons, or commas for certain, and other non-alphanumeric characters are also probably invalid). You should 066 * not assume anything is present in the Preferences storage unless you have put it there, and this applies doubly 067 * to games or applications other than your own; you should avoid values for fileName that might overlap with 068 * another game's Preferences values. This constructor also allows you to specify a "garble" String; if this is 069 * non-null, it will be used as a key to obfuscate the output and de-obfuscate the loaded input using fairly basic 070 * methods. If garble is null, it is ignored. 071 * <br> 072 * To organize saved data into sub-sections, you specify logical units (like different players' saved games) with a 073 * String outerName when you call {@link #store(String)}, and can further distinguish data under the outerName when 074 * you call {@link #put(String, Object, StringConvert)} to put each individual item into the saved storage with its 075 * own innerName. 076 * <br> 077 * Calling this also sets up custom serializers for several important types in SquidLib; char[][], OrderedMap, 078 * IntDoubleOrderedMap, FakeLanguageGen, GreasedRegion, and notably Pattern from RegExodus all have smaller 079 * serialized representations than the default. OrderedMap allows non-String keys, which gets around a limitation in 080 * JSON maps normally, and both FakeLanguageGen and Pattern are amazingly smaller with the custom representation. 081 * The custom char[][] representation is about half the normal size by omitting commas after each char. 082 * @param fileName the valid file name to create or open from Preferences; typically the name of the game/app. 083 * @param garble the key that must be used exactly to decrypt any data saved by this TextStorage 084 */ 085 public TextStorage(final String fileName, final String garble) 086 { 087 storageName = fileName; 088 preferences = Gdx.app.getPreferences(storageName); 089 contents = new OrderedMap<>(16, 0.2f); 090 mapConverter = Converters.convertOrderedMap(Converters.convertString, Converters.convertString); 091 garbleKey = Garbler.makeKeyArray(5, garble); 092 } 093 094 /** 095 * Creates a JsonStorage with the given fileName to save using Preferences from libGDX. The name should generally 096 * be the name of this game or application, and must be a valid name for a file (so no slashes, backslashes, colons, 097 * semicolons, or commas for certain, and other non-alphanumeric characters are also probably invalid). You should 098 * not assume anything is present in the Preferences storage unless you have put it there, and this applies doubly 099 * to games or applications other than your own; you should avoid values for fileName that might overlap with 100 * another game's Preferences values. This constructor also allows you to specify a "garble" long array; if this is 101 * non-empty, it will be used as a key to obfuscate the output and de-obfuscate the loaded input using fairly basic 102 * methods. If garble is null or empty, it is ignored. 103 * <br> 104 * To organize saved data into sub-sections, you specify logical units (like different players' saved games) with a 105 * String outerName when you call {@link #store(String)}, and can further distinguish data under the outerName when 106 * you call {@link #put(String, Object, StringConvert)} to put each individual item into the saved storage with its 107 * own innerName. 108 * <br> 109 * Calling this also sets up custom serializers for several important types in SquidLib; char[][], OrderedMap, 110 * IntDoubleOrderedMap, FakeLanguageGen, GreasedRegion, and notably Pattern from RegExodus all have smaller 111 * serialized representations than the default. OrderedMap allows non-String keys, which gets around a limitation in 112 * JSON maps normally, and both FakeLanguageGen and Pattern are amazingly smaller with the custom representation. 113 * The custom char[][] representation is about half the normal size by omitting commas after each char. 114 * @param fileName the valid file name to create or open from Preferences; typically the name of the game/app. 115 * @param garble the key that must be used exactly to decrypt any data saved by this TextStorage; will be copied 116 */ 117 public TextStorage(final String fileName, final long[] garble) { 118 storageName = fileName; 119 preferences = Gdx.app.getPreferences(storageName); 120 contents = new OrderedMap<>(16, 0.2f); 121 mapConverter = Converters.convertOrderedMap(Converters.convertString, Converters.convertString); 122 if (garble == null || garble.length == 0) 123 garbleKey = null; 124 else { 125 garbleKey = new long[garble.length]; 126 System.arraycopy(garble, 0, garbleKey, 0, garble.length); 127 } 128 } 129 130 /** 131 * Prepares to store the Object {@code o} to be retrieved with {@code innerName} in the current group of objects. 132 * Does not write to a permanent location until {@link #store(String)} is called. The innerName used to store an 133 * object is required to get it back again, and can also be used to remove it before storing (or storing again). 134 * @param innerName one of the two Strings needed to retrieve this later 135 * @param o the Object to prepare to store 136 * @param converter a StringConvert that supports the type of o 137 * @return this for chaining 138 */ 139 @SuppressWarnings("unchecked") 140 public <T> TextStorage put(String innerName, T o, StringConvert converter) 141 { 142 contents.put(innerName, (o == null) ? "" : converter.stringify(o)); 143 return this; 144 } 145 146 /** 147 * Actually stores all objects that had previously been prepared with {@link #put(String, Object, StringConvert)}, 148 * with {@code outerName} used as a key to retrieve any object in the current group. Flushes the preferences, making 149 * the changes permanent (until overwritten), but does not change the current group (you may want to call this 150 * method again with additional items in the current group, and that would simply involve calling put() again). If 151 * you want to clear the current group, use {@link #clear()}. If you want to remove just one object from the current 152 * group, use {@link #remove(String)}. 153 * @param outerName one of the two Strings needed to retrieve any of the objects in the current group 154 * @return this for chaining 155 */ 156 public TextStorage store(String outerName) 157 { 158 if(garbleKey == null) { 159 if (compress) 160 preferences.putString(outerName, LZSPlus.compress(mapConverter.stringify(contents))); 161 else 162 preferences.putString(outerName, mapConverter.stringify(contents)); 163 } 164 else 165 { 166 if (compress) 167 preferences.putString(outerName, LZSPlus.compress(mapConverter.stringify(contents), garbleKey)); 168 else 169 preferences.putString(outerName, Garbler.garble(mapConverter.stringify(contents), garbleKey)); 170 } 171 preferences.flush(); 172 return this; 173 } 174 175 /** 176 * Gets a String representation of the data that would be saved when {@link #store(String)} is called. This can be 177 * useful for finding particularly problematic objects that require unnecessary space when serialized. 178 * @return a String that previews what would be stored permanently when {@link #store(String)} is called 179 */ 180 public String show() 181 { 182 183 if(garbleKey == null) { 184 if (compress) 185 return LZSPlus.compress(mapConverter.stringify(contents)); 186 else 187 return mapConverter.stringify(contents); 188 } 189 else 190 { 191 if (compress) 192 return LZSPlus.compress(mapConverter.stringify(contents), garbleKey); 193 else 194 return Garbler.garble(mapConverter.stringify(contents), garbleKey); 195 } 196 } 197 198 /** 199 * Clears the current group of objects; recommended if you intend to store under multiple outerName keys. 200 * @return this for chaining 201 */ 202 public TextStorage clear() 203 { 204 contents.clear(); 205 return this; 206 } 207 208 /** 209 * Removes one object from the current group by the {@code innerName} it was prepared with using 210 * {@link #put(String, Object, StringConvert)}. This does not affect already-stored objects unless 211 * {@link #store(String)} is called after this, in which case the new version of the current group, without the 212 * object this removed, is stored. 213 * @param innerName the String key used to put an object in the current group with {@link #put(String, Object, StringConvert)} 214 * @return this for chaining 215 */ 216 public TextStorage remove(String innerName) 217 { 218 contents.remove(innerName); 219 return this; 220 } 221 222 /** 223 * Gets an object from the storage by the given {@code outerName} key from {@link #store(String)} and 224 * {@code innerName} key from {@link #put(String, Object, StringConvert)}, and uses the class given by {@code type} 225 * for the returned value, assuming it matches the object that was originally put with those keys. If no such object 226 * exists, returns null. Results are undefined if {@code type} doesn't match the actual class of the stored object. 227 * @param outerName the key used to store the group of objects with {@link #store(String)} 228 * @param innerName the key used to store the specific object with {@link #put(String, Object, StringConvert)} 229 * @param converter 230 * a StringConvert, such as one from {@link Converters} or found with 231 * {@link StringConvert#get(CharSequence)}, to deserialize the data 232 * @param type the class of the value; for a class like RNG, use {@code RNG.class}, but changed to fit 233 * @param <T> the type of the value to retrieve; if type was {@code RNG.class}, this would be {@code RNG} 234 * @return the retrieved value if successful, or null otherwise 235 */ 236 public <T> T get(String outerName, String innerName, StringConvert<?> converter, Class<T> type) 237 { 238 OrderedMap<String, String> om; 239 String got; 240 if(garbleKey == null) { 241 if (compress) 242 got = LZSPlus.decompress(preferences.getString(outerName)); 243 else 244 got = preferences.getString(outerName); 245 } 246 else 247 { 248 if (compress) 249 got = LZSPlus.decompress(preferences.getString(outerName), garbleKey); 250 else 251 got = Garbler.degarble(preferences.getString(outerName), garbleKey); 252 } 253 if(got == null) return null; 254 om = mapConverter.restore(got); 255 if(om == null) return null; 256 return converter.restore(om.get(innerName), type); 257 } 258 259 /** 260 * Gets an object from the storage by the given {@code outerName} key from {@link #store(String)} and 261 * {@code innerName} key from {@link #put(String, Object, StringConvert)}, and uses the class given by {@code type} 262 * for the returned value, assuming it matches the object that was originally put with those keys. Uses typeName to 263 * find an appropriate StringConvert that has already been created (and thus registered), and because typeName is a 264 * CharSequence instead of a Class, it doesn't suffer from generic type erasure at runtime, It can and should have 265 * the generic type arguments as if it were the type for a variable, e.g. {@code OrderedSet<ArrayList<String>>}. If 266 * no such object exists, returns null. Results are undefined if {@code type} doesn't match the actual class of the 267 * stored object, and this will return null if there is no known StringConvert for the given typeName. 268 * @param outerName the key used to store the group of objects with {@link #store(String)} 269 * @param innerName the key used to store the specific object with {@link #put(String, Object, StringConvert)} 270 * @param typeName the name of the type to produce, with generic type parameters intact; used to find an appropriate StringConvert 271 * @param type the class of the value; for a class like RNG, use {@code RNG.class}, but changed to fit 272 * @param <T> the type of the value to retrieve; if type was {@code RNG.class}, this would be {@code RNG} 273 * @return the retrieved value if successful, or null otherwise 274 */ 275 public <T> T get(String outerName, String innerName, CharSequence typeName, Class<T> type) 276 { 277 OrderedMap<String, String> om; 278 String got; 279 if(garbleKey == null) { 280 if (compress) 281 got = LZSPlus.decompress(preferences.getString(outerName)); 282 else 283 got = preferences.getString(outerName); 284 } 285 else 286 { 287 if (compress) 288 got = LZSPlus.decompress(preferences.getString(outerName), garbleKey); 289 else 290 got = Garbler.degarble(preferences.getString(outerName), garbleKey); 291 } 292 if(got == null) return null; 293 om = mapConverter.restore(got); 294 if(om == null) return null; 295 StringConvert<?> converter = StringConvert.get(typeName); 296 if(converter == null) return null; 297 got = om.get(innerName); 298 if(got == null) return null; 299 return converter.restore(got, type); 300 } 301 302 /** 303 * Gets the approximate size of the currently-stored preferences. This assumes UTF-16 storage, which is the case for 304 * GWT's LocalStorage. Since GWT is restricted to the size the browser permits for LocalStorage, and this limit can 305 * be rather small (about 5 MB, sometimes more but not reliably), this method is especially useful there, but it may 306 * yield inaccurate sizes on other platforms that save Preferences data differently. 307 * @return the size, in bytes, of the already-stored preferences 308 */ 309 public int preferencesSize() 310 { 311 Map<String, ?> p = preferences.get(); 312 int byteSize = 0; 313 for(String k : p.keySet()) 314 { 315 byteSize += k.length(); 316 byteSize += preferences.getString(k, "").length(); 317 } 318 return byteSize * 2; 319 } 320 321}