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.


What is persistent savedata?

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.

How do I use persistent savedata?

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.

Persistent savedata is XML

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.


Persistent savedata can be externally modified

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.

Viewing & editing persistent savedata

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.


Crash course for using XElement

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 name parameter is actually an XName, but you can supply a string that will be converted to an XName.
  • 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.
  • XElement.Element(string name)

    • Retrieves a child XElement inside the calling XElement using the specified child element name.
    • Returns null if no such element exists.
    • The name parameter is actually an XName, but you can supply a string that will be converted to an XName.
    • Use this for XElements that have other XElement(s) as children.
  • XElement.Elements()

    • Returns a collection of all child XElements that are inside the calling XElement.
    • Use this on XElements that constitute a list or dictionary of other XElements.
  • XElement.Add(object content)

    • Adds the provided object to the calling XElement's children. Typically used to put more XElements inside the calling XElement.
    • Use this to add new XElements to another XElement that constitutes a list or dictionary of other XElements.
  • 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 name parameter is actually an XName, but you can supply a string that will be converted to an XName.
    • Use this to decorate an XElement will a small piece of data, like an ID or type.
  • XElement.Attribute(string name)

    • Retrieves the XAttribute specified by the provided name on the calling XElement.
    • Returns null if no such attribute exists.
    • The name parameter is actually an XName, but you can supply a string that will be converted to an XName.
    • Use this to check for the existence of an attribute and then retrieve its value (using XAttribute.Value).
  • Another useful XML class is the XComment, which allows you to create XML comments <!--like this-->. You can add XComments to any XElement using XElement.Add(XComment).

Example usage

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 });
		}
	}
}

Full plugin example with persistent savedata

The PersistentSavedataDemo example plugin is a complete demo of how to use persistent savedata in a meaningful way.


Next

Read the Helper methods article to learn about the various helper methods that your plugin can use.

⚠️ **GitHub.com Fallback** ⚠️