using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
namespace StardewModdingAPI.Framework.ModHelpers
{
/// Provides an API for reading and storing local mod data.
internal class DataHelper : BaseHelper, IDataHelper
{
/*********
** Fields
*********/
/// Encapsulates SMAPI's JSON file parsing.
private readonly JsonHelper JsonHelper;
/// The absolute path to the mod folder.
private readonly string ModFolderPath;
/*********
** Public methods
*********/
/// Construct an instance.
/// The unique ID of the relevant mod.
/// The absolute path to the mod folder.
/// The absolute path to the mod folder.
public DataHelper(string modID, string modFolderPath, JsonHelper jsonHelper)
: base(modID)
{
this.ModFolderPath = modFolderPath;
this.JsonHelper = jsonHelper;
}
/****
** JSON file
****/
///
public TModel? ReadJsonFile(string path)
where TModel : class
{
if (!PathUtilities.IsSafeRelativePath(path))
throw new InvalidOperationException($"You must call {nameof(IModHelper.Data)}.{nameof(this.ReadJsonFile)} with a relative path.");
path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path));
return this.JsonHelper.ReadJsonFileIfExists(path, out TModel? data)
? data
: null;
}
///
public void WriteJsonFile(string path, TModel? data)
where TModel : class
{
if (!PathUtilities.IsSafeRelativePath(path))
throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing).");
path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path));
if (data != null)
this.JsonHelper.WriteJsonFile(path, data);
else
File.Delete(path);
}
/****
** Save file
****/
///
public TModel? ReadSaveData(string key)
where TModel : class
{
if (Context.LoadStage == LoadStage.None)
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded.");
if (!Context.IsOnHostComputer)
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when connected to a remote host. (Save files are stored on the main player's computer.)");
string internalKey = this.GetSaveFileKey(key);
foreach (IDictionary dataField in this.GetDataFields(Context.LoadStage))
{
if (dataField.TryGetValue(internalKey, out string? value))
return this.JsonHelper.Deserialize(value);
}
return null;
}
///
public void WriteSaveData(string key, TModel? model)
where TModel : class
{
if (Context.LoadStage == LoadStage.None)
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded.");
if (!Context.IsOnHostComputer)
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when connected to a remote host. (Save files are stored on the main player's computer.)");
string internalKey = this.GetSaveFileKey(key);
string? data = model != null
? this.JsonHelper.Serialize(model, Formatting.None)
: null;
foreach (IDictionary dataField in this.GetDataFields(Context.LoadStage))
{
if (data != null)
dataField[internalKey] = data;
else
dataField.Remove(internalKey);
}
}
/****
** Global app data
****/
///
public TModel? ReadGlobalData(string key)
where TModel : class
{
string path = this.GetGlobalDataPath(key);
return this.JsonHelper.ReadJsonFileIfExists(path, out TModel? data)
? data
: null;
}
///
public void WriteGlobalData(string key, TModel? data)
where TModel : class
{
string path = this.GetGlobalDataPath(key);
if (data != null)
this.JsonHelper.WriteJsonFile(path, data);
else
File.Delete(path);
}
/*********
** Public methods
*********/
/// Get the unique key for a save file data entry.
/// The unique key identifying the data.
private string GetSaveFileKey(string key)
{
this.AssertSlug(key, nameof(key));
return $"smapi/mod-data/{this.ModID}/{key}".ToLower();
}
/// Get the data fields to read/write for save data.
/// The current load stage.
private IEnumerable> GetDataFields(LoadStage stage)
{
if (stage == LoadStage.None)
yield break;
yield return Game1.CustomData;
if (SaveGame.loaded != null)
yield return SaveGame.loaded.CustomData;
}
/// Get the absolute path for a global data file.
/// The unique key identifying the data.
private string GetGlobalDataPath(string key)
{
this.AssertSlug(key, nameof(key));
return Path.Combine(Constants.DataPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower());
}
/// Assert that a key contains only characters that are safe in all contexts.
/// The key to check.
/// The argument name for any assertion error.
private void AssertSlug(string key, string paramName)
{
if (!PathUtilities.IsSlug(key))
throw new ArgumentException("The data key is invalid (keys must only contain letters, numbers, underscores, periods, or hyphens).", paramName);
}
}
}