package squidpony.store.json;
import blazing.chain.LZSEncoding;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Preferences;
import com.badlogic.gdx.utils.JsonWriter;
import squidpony.StringStringMap;
import squidpony.annotation.Beta;
import java.util.Map;
/**
* Helps games store information in libGDX's Preferences class as Strings, then get it back out.
* Created by Tommy Ettinger on 9/16/2016.
*/
@Beta
public class JsonStorage {
public final Preferences preferences;
public final String storageName;
public final JsonConverter json;
protected StringStringMap contents;
public boolean compress = true;
/**
* Please don't use this constructor if possible; it simply calls {@link #JsonStorage(String)} with the constant
* String "nameless". This could easily overlap with other files/sections in Preferences, so you should always
* prefer giving a String argument to the constructor, typically the name of the game.
* @see #JsonStorage(String) the recommended constructor to use
*/
public JsonStorage()
{
this("nameless");
}
/**
* Creates a JsonStorage with the given fileName to save using Preferences from libGDX. The name should generally
* be the name of this game or application, and must be a valid name for a file (so no slashes, backslashes, colons,
* semicolons, or commas for certain, and other non-alphanumeric characters are also probably invalid). You should
* not assume anything is present in the Preferences storage unless you have put it there, and this applies doubly
* to games or applications other than your own; you should avoid values for fileName that might overlap with
* another game's Preferences values.
* <br>
* To organize saved data into sub-sections, you specify logical units (like different players' saved games) with a
* String outerName when you call {@link #store(String)}, and can further distinguish data under the outerName when
* you call {@link #put(String, Object)} to put each individual item into the saved storage with its own innerName.
* <br>
* Calling this also sets up custom serializers for several important types in SquidLib; char[][], OrderedMap,
* IntDoubleOrderedMap, FakeLanguageGen, GreasedRegion, and notably Pattern from RegExodus all have smaller
* serialized representations than the default. OrderedMap allows non-String keys, which gets around a limitation in
* JSON maps normally, and both FakeLanguageGen and Pattern are amazingly smaller with the custom representation.
* The custom char[][] representation is about half the normal size by omitting commas after each char.
* @param fileName the valid file name to create or open from Preferences; typically the name of the game/app.
*/
public JsonStorage(final String fileName)
{
storageName = fileName;
preferences = Gdx.app.getPreferences(storageName);
json = new JsonConverter(JsonWriter.OutputType.minimal);
contents = new StringStringMap(16, 0.2f);
}
/**
* Prepares to store the Object {@code o} to be retrieved with {@code innerName} in the current group of objects.
* Does not write to a permanent location until {@link #store(String)} is called. The innerName used to store an
* object is required to get it back again, and can also be used to remove it before storing (or storing again).
* @param innerName one of the two Strings needed to retrieve this later
* @param o the Object to prepare to store
* @return this for chaining
*/
public JsonStorage put(String innerName, Object o)
{
contents.put(innerName, json.toJson(o));
return this;
}
/**
* Actually stores all objects that had previously been prepared with {@link #put(String, Object)}, with
* {@code outerName} used as a key to retrieve any object in the current group. Flushes the preferences, making the
* changes permanent (until overwritten), but does not change the current group (you may want to call this method
* again with additional items in the current group, and that would simply involve calling put() again). If you want
* to clear the current group, use {@link #clear()}. If you want to remove just one object from the current group,
* use {@link #remove(String)}.
* @param outerName one of the two Strings needed to retrieve any of the objects in the current group
* @return this for chaining
*/
public JsonStorage store(String outerName)
{
if(compress)
preferences.putString(outerName, LZSEncoding.compressToUTF16(json.toJson(contents, StringStringMap.class)));
else
preferences.putString(outerName, json.toJson(contents, StringStringMap.class));
preferences.flush();
return this;
}
/**
* Gets a String representation of the data that would be saved when {@link #store(String)} is called. This can be
* useful for finding particularly problematic objects that require unnecessary space when serialized.
* @return a String that previews what would be stored permanently when {@link #store(String)} is called
*/
public String show()
{
if(compress)
return LZSEncoding.compressToUTF16(json.toJson(contents, StringStringMap.class));
else
return json.toJson(contents, StringStringMap.class);
}
/**
* Clears the current group of objects; recommended if you intend to store under multiple outerName keys.
* @return this for chaining
*/
public JsonStorage clear()
{
contents.clear();
return this;
}
/**
* Removes one object from the current group by the {@code innerName} it was prepared with using
* {@link #put(String, Object)}. This does not affect already-stored objects unless {@link #store(String)} is called
* after this, in which case the new version of the current group, without the object this removed, is stored.
* @param innerName the String key used to put an object in the current group with {@link #put(String, Object)}
* @return this for chaining
*/
public JsonStorage remove(String innerName)
{
contents.remove(innerName);
return this;
}
/**
* Gets an object from the storage by the given {@code outerName} key from {@link #store(String)} and
* {@code innerName} key from {@link #put(String, Object)}, and uses the class given by {@code type} for the
* returned value, assuming it matches the object that was originally put with those keys. If no such object is
* present, returns null. Results are undefined if {@code type} doesn't match the actual class of the stored object.
* @param outerName the key used to store the group of objects with {@link #store(String)}
* @param innerName the key used to store the specific object with {@link #put(String, Object)}
* @param type the class of the value; for a class like RNG, use {@code RNG.class}, but changed to fit
* @param <T> the type of the value to retrieve; if type was {@code RNG.class}, this would be {@code RNG}
* @return the retrieved value if successful, or null otherwise
*/
@SuppressWarnings("unchecked")
public <T> T get(String outerName, String innerName, Class<T> type)
{
StringStringMap om;
String got;
if(compress)
got = LZSEncoding.decompressFromUTF16(preferences.getString(outerName));
else
got = preferences.getString(outerName);
if(got == null) return null;
om = json.fromJson(StringStringMap.class, got);
if(om == null) return null;
return json.fromJson(type, om.get(innerName));
}
/**
* Gets the approximate size of the currently-stored preferences. This assumes UTF-16 storage, which is the case for
* GWT's LocalStorage. Since GWT is restricted to the size the browser permits for LocalStorage, and this limit can
* be rather small (about 5 MB, sometimes more but not reliably), this method is especially useful there, but it may
* yield inaccurate sizes on other platforms that save Preferences data differently.
* @return the size, in bytes, of the already-stored preferences
*/
public int preferencesSize()
{
Map<String, ?> p = preferences.get();
int byteSize = 0;
for(String k : p.keySet())
{
byteSize += k.length();
byteSize += preferences.getString(k, "").length();
}
return byteSize * 2;
}
}