package squidpony.store.text;
import blazing.chain.LZSEncoding;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Preferences;
import squidpony.Converters;
import squidpony.StringConvert;
import squidpony.annotation.Beta;
import squidpony.squidmath.OrderedMap;
import java.util.Map;
/**
* Helps games store information in libGDX's Preferences class as Strings, then get it back out. Does not use JSON,
* instead using a customized and customizable manual serialization style based around {@link StringConvert}.
* Created by Tommy Ettinger on 9/16/2016.
*/
@Beta
public class TextStorage {
public final Preferences preferences;
public final String storageName;
protected OrderedMap<String, String> contents;
public final StringConvert<OrderedMap<String, String>> mapConverter;
public boolean compress = true;
/**
* Please don't use this constructor if possible; it simply calls {@link #TextStorage(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 #TextStorage(String) the recommended constructor to use
*/
public TextStorage()
{
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, StringConvert)} 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 TextStorage(final String fileName)
{
storageName = fileName;
preferences = Gdx.app.getPreferences(storageName);
contents = new OrderedMap<>(16, 0.2f);
mapConverter = Converters.convertOrderedMap(Converters.convertString, Converters.convertString);
}
/**
* 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
* @param converter a StringConvert that supports the type of o
* @return this for chaining
*/
@SuppressWarnings("unchecked")
public <T> TextStorage put(String innerName, T o, StringConvert converter)
{
contents.put(innerName, (o == null) ? "" : converter.stringify(o));
return this;
}
/**
* Actually stores all objects that had previously been prepared with {@link #put(String, Object, StringConvert)},
* 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 TextStorage store(String outerName)
{
if(compress)
preferences.putString(outerName, LZSEncoding.compressToUTF16(mapConverter.stringify(contents)));
else
preferences.putString(outerName, mapConverter.stringify(contents));
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(mapConverter.stringify(contents));
else
return mapConverter.stringify(contents);
}
/**
* Clears the current group of objects; recommended if you intend to store under multiple outerName keys.
* @return this for chaining
*/
public TextStorage 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, StringConvert)}. 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, StringConvert)}
* @return this for chaining
*/
public TextStorage 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, StringConvert)}, 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
* exists, 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, StringConvert)}
* @param converter
* a StringConvert, such as one from {@link Converters} or found with
* {@link StringConvert#get(CharSequence)}, to deserialize the data
* @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, StringConvert<?> converter, Class<T> type)
{
OrderedMap<String, String> om;
String got;
if(compress)
got = LZSEncoding.decompressFromUTF16(preferences.getString(outerName));
else
got = preferences.getString(outerName);
if(got == null) return null;
om = mapConverter.restore(got);
if(om == null) return null;
return converter.restore(om.get(innerName), type);
}
/**
* 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, StringConvert)}, and uses the class given by {@code type}
* for the returned value, assuming it matches the object that was originally put with those keys. Uses typeName to
* find an appropriate StringConvert that has already been created (and thus registered), and because typeName is a
* CharSequence instead of a Class, it doesn't suffer from generic type erasure at runtime, It can and should have
* the generic type arguments as if it were the type for a variable, e.g. {@code OrderedSet<ArrayList<String>>}. If
* no such object exists, returns null. Results are undefined if {@code type} doesn't match the actual class of the
* stored object, and this will return null if there is no known StringConvert for the given typeName.
* @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, StringConvert)}
* @param typeName the name of the type to produce, with generic type parameters intact; used to find an appropriate StringConvert
* @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, CharSequence typeName, Class<T> type)
{
OrderedMap<String, String> om;
String got;
if(compress)
got = LZSEncoding.decompressFromUTF16(preferences.getString(outerName));
else
got = preferences.getString(outerName);
if(got == null) return null;
om = mapConverter.restore(got);
if(om == null) return null;
StringConvert<?> converter = StringConvert.get(typeName);
if(converter == null) return null;
got = om.get(innerName);
if(got == null) return null;
return converter.restore(got, type);
}
/**
* 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;
}
}