Using persistent savedata - TiberiumFusion/TTPlugins GitHub Wiki
This section explains how to enable Persistent Savedata for your plugin and provides a crash course in Linq XML.
For a complete demo of a plugin that uses persistent savedata in a meaningful way, please refer to the PersistentSavedataDemo example plugin.
When a user enables a plugin in one of their Tweak Lists, an XML element is created inside that Tweak List to store data for the plugin. During Terraria gameplay, the plugin can modify an in-memory copy of that XML element to read and write new savedata. When the user exits Terraria normally (i.e. the Exit button on the main menu), the in-memory copy of the XML is written back into the Tweak List. In this way, a plugin can store and retrieve persistent data between Terraria launches without needing to manage a custom savedata solution.
Persistent savedata is useful for:
- Storing user preferences, such as the intensity of a configurable speed boost plugin
- Keeping statistics, like tracking the player's lifetime monster kills
- Maintaining state information for the plugin between Terraria launches
NOTE: To remain consistent with how & when Terraria saves things to disk, persistent savedata is only saved when Terraria closes normally. If the user kills the Terraria process or uses Alt+F4, the on-disk persistent savedata will not be updated.
Enabling persistent savedata is very simple. In your plugin's Initialize() method, set the plugin's HasPersistentSavedata property to true. When your plugin's ConfigurationLoaded() method is called, you can retrieve the persistent savedata from the Configuration.Savedata property.
In contrast, if HasPersistentSavedata is set to false, your plugin will receive a blank Configuration.Savedata element, and any changes you made to that element will not be written back to disk when Terraria closes.
To work with persistent savedata, you will use the the "LINQ to XML" classes in .NET, namely XElement. Your plugin's Configuration.Savedata property is an XElement that might look something like this:
<Savedata>
<!--This <Savedata> element contains the persistent savedata for this plugin.-->
<!--If you are manually editing this savedata, do NOT modify the top-level <Savedata> element!-->
</Savedata>That <Savedata> tag (aka element), plus its content, equals your plugin's Configuration.Savedata XElement. You can put anything you want inside the Savedata element. The data inside will be maintained between Terraria launches.
IMPORTANT: Do not rename the Savedata object to have a different tag name.
Using Terraria Tweaker 2's plugin manager, the end user can modify the persistent savedata of any plugin at their discretion. In other words, the end user can mismanage and corrupt your plugin's persistent savedata. So, when your plugin reads its persistent savedata, always check for null values and invalid data! Your plugin will crash easily if it doesn't parse the XML in a safe manner.
You don't have to validate the top-level <Savedata> element. If <Savedata> element is missing, the plugin framework will generate a new (empty) one before providing it to your plugin.
With Terraria Tweaker 2, you can inspect and modify the persistent savedata stored for each plugin inside your Tweak Lists. This may aid in debugging during plugin development.
Every XML node in an XML tree is an XElement that you can manipulate.
XElement has many members, but these the 7 members that you will (probably) commonly use:
-
new XElement(string name, object value)- Creates a new XElement with the provided name and value.
- For example,
new XElement("SomeData", 123)will serialize into<SomeData>123</SomeData> - The
nameparameter is actually anXName, but you can supply a string that will be converted to anXName.
-
XElement.Value- Gets or sets the string value that is inside this
XElement - Use this for "dead-end"
XElements that have plain data inside them.
- Gets or sets the string value that is inside this
-
XElement.Element(string name)- Retrieves a child
XElementinside the callingXElementusing the specified child element name. - Returns null if no such element exists.
- The
nameparameter is actually anXName, but you can supply a string that will be converted to anXName. - Use this for
XElements that have otherXElement(s) as children.
- Retrieves a child
-
XElement.Elements()- Returns a collection of all child
XElementsthat are inside the callingXElement. - Use this on
XElements that constitute a list or dictionary of otherXElements.
- Returns a collection of all child
-
XElement.Add(object content)- Adds the provided object to the calling
XElement's children. Typically used to put moreXElements inside the callingXElement. - Use this to add new
XElements to anotherXElementthat constitutes a list or dictionary of otherXElements.
- Adds the provided object to the calling
-
XElement.SetAttributeValue(string name, object value)- Sets the value of the attribute specified by name for the calling
XElement. Will create a new attribute if it does not exist. - The
nameparameter is actually anXName, but you can supply a string that will be converted to anXName. - Use this to decorate an
XElementwill a small piece of data, like an ID or type.
- Sets the value of the attribute specified by name for the calling
-
XElement.Attribute(string name)- Retrieves the
XAttributespecified by the providednameon the callingXElement. - Returns null if no such attribute exists.
- The
nameparameter is actually anXName, but you can supply a string that will be converted to anXName. - Use this to check for the existence of an attribute and then retrieve its value (using
XAttribute.Value).
- Retrieves the
-
Another useful XML class is the
XComment, which allows you to create XML comments<!--like this-->. You can addXCommentsto anyXElementusingXElement.Add(XComment).
This code will generate some XML:
Configuration.Savedata.RemoveAll();
Configuration.Savedata.Add(new XElement("BoostPower", 10f));
XElement boostTime = new XElement("BoostTime");
boostTime.SetAttributeValue("Time", 50);
Configuration.Savedata.Add(boostTime);
XElement boostItems = new XElement("BoostItems");
int[] ids = new int[] { 1, 2, 7, 41, 3920 };
foreach (int id in ids)
{
XElement item = new XElement("Item");
item.SetAttributeValue("ID", id);
item.SetAttributeValue("Type", id < 1000 ? "Primary" : "Secondary");
boostItems.Add(item);
}
Configuration.Savedata.Add(boostItems);Here's the XML that is produced:
<Savedata>
<BoostPower>10</BoostPower>
<BoostTime Time="10"/>
<BoostItems>
<Item ID="1" Type="Primary"/>
<Item ID="2" Type="Primary"/>
<Item ID="7" Type="Primary"/>
<Item ID="41" Type="Primary"/>
<Item ID="3920" Type="Secondary"/>
</BoostItems>
</Savedata>And finally, here's more code which safely reads the above XML:
int BoostTime;
float BoostPower;
List<BoostItem> BoostItems;
class BoostItem
{
public int ID = 0;
public string Type = null;
}
void SafelyLoadXML()
{
BoostPower = 0f;
BoostTime = 0;
BoostItems = new List<BoostItem>();
BoostPower = float.Parse(Configuration.Savedata.Element("BoostPower")?.Value ?? "0");
BoostTime = int.Parse(Configuration.Savedata.Element("BoostTime")?.Value ?? "0");
XElement xmlBoostItems = Configuration.Savedata.Element("BoostItems");
if (xmlBoostItems != null)
{
foreach (XElement xmlBoostItem in xmlBoostItems.Elements())
{
int itemID = 0;
XAttribute idAttr = xmlBoostItem.Attribute("ID");
if (idAttr != null && idAttr.Value != null)
{
if (!int.TryParse(idAttr.Value, out itemID))
continue;
}
else
continue;
string itemType = null;
XAttribute typeAttr = xmlBoostItem.Attribute("Type");
if (typeAttr != null && typeAttr.Value != null)
itemType = typeAttr.Value.ToString();
else
continue;
BoostItems.Add(new BoostItem() { ID = itemID, Type = itemType });
}
}
}The PersistentSavedataDemo example plugin is a complete demo of how to use persistent savedata in a meaningful way.
Read the Helper methods article to learn about the various helper methods that your plugin can use.