using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
namespace Framework.Storage {
/// Provides access to the standard directories in the current OS
public class StandardDirectories {
/// Name that should be used for the game's directories
public static string GameDirectoryName {
get { return sanitizeFilename(Application.productName); }
}
/// Location of the game's assets and runtime data
public static string GameDataPath {
get { return Application.dataPath; }
}
/// Location where the game should save user-specific settings and data
public static string UserDataPath {
get {
switch(Environment.OSVersion.Platform) {
// Linux systems
// -------------
//
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
// The correct choice would be XDG_DATA_HOME/game/
// Unity interprets the XDG spec as XDG_CONFIG_HOME/game/
// So we use Environment.GetFolderPath() to do our own lookup
case PlatformID.Unix: {
string localApplicationDataPath = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData
);
return Path.Combine(localApplicationDataPath, GameDirectoryName);
}
// Windows systems
// ---------------
//
// User-specific game data goes into $User/My Documents/My Games/game/
// like done by a number of AAA titles.
case PlatformID.Win32NT:
case PlatformID.Win32S:
case PlatformID.Win32Windows:
case PlatformID.WinCE: {
string myGamesPath;
{
string myDocumentsPath = Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments
);
myGamesPath = Path.Combine(myDocumentsPath, "My Games");
}
return Path.Combine(myGamesPath, GameDirectoryName);
}
// Unknown (probably mobile)
// -------------------------
//
// We don't know this platform and trust Unity to provide us with
// the correct path for the game's user data.
default: {
return Application.persistentDataPath;
}
}
}
}
/// Location where the game can save game states and player profiles
public static string SaveGamePath {
get {
switch(Environment.OSVersion.Platform) {
// Linux systems
// -------------
//
// We use a 'Saves' folder inside the standard user data path
case PlatformID.Unix: {
return Path.Combine(UserDataPath, "Saves");
}
// Windows systems
// ---------------
//
// The correct choice would be $User/Saved Games/game/ but this path has
// not arrived in .NET yet and is only supported on Vista & later, so we try
// to find it and otherwise fall back to the inofficial standard of putting
// saved games into a 'Saves' folder inside the game's normal data directory.
case PlatformID.Win32NT:
case PlatformID.Win32S:
case PlatformID.Win32Windows:
case PlatformID.WinCE: {
{
string savedGamesPath = locateSavedGamesFolder();
if(savedGamesPath != null) {
return Path.Combine(savedGamesPath, GameDirectoryName);
}
}
// If the 'Saved Games' folder wasn't found, fall back to the 'Saves' folder
return Path.Combine(UserDataPath, "Saves");
}
// Unknown (probably mobile)
// -------------------------
//
// We don't know this platform, so we rely on the normal user data path
// and set up a 'Saves' folder inside it
default: {
return Path.Combine(UserDataPath, "Saves");
}
}
}
}
/// Locates the path of the Windows 'Saved Games' folder
/// The path of the 'Saved Games' folder or null if not found
private static string locateSavedGamesFolder() {
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
// Attempt 1: use the SHGetKnownFolderPath() method from the WIN32 API
{
IntPtr pathPointer;
int resultHandle = UnsafeNativeMethods.SHGetKnownFolderPath(
UnsafeNativeMethods.FOLDERID_SavedGames, 0, IntPtr.Zero, out pathPointer
);
if(resultHandle >= 0) {
try {
//Debug.Log("SHGetKnownFolderPath succeeded!");
return Marshal.PtrToStringUni(pathPointer);
}
finally {
Marshal.FreeCoTaskMem(pathPointer);
}
}
}
#endif // UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
// Attempt 2: look for the folder in its default location
{
string savedGamesPath;
{
string userPath;
{
string myDocumentsPath = Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments
);
userPath = Path.GetDirectoryName(myDocumentsPath);
}
savedGamesPath = Path.Combine(userPath, "Saved Games");
}
if(Directory.Exists(savedGamesPath)) {
return savedGamesPath;
}
}
// We failed.
return null;
}
/// Characters that are legal in a sanitized filename
private static readonly char[] saneCharacters = new char[] {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-'
};
/// Removes trouble-causing characters from a filename
/// Filename that will be sanitized
/// A sanitized version of the specified filename
private static string sanitizeFilename(string filename) {
if(string.IsNullOrEmpty(filename)) {
return filename;
}
// Create a string builder to form the sanitized file name in
int filenameLength = filename.Length;
var saneFilenameBuilder = new StringBuilder(capacity: filenameLength);
// Append sane characters to the result, replace other characters
// with one '-' sign (except in front, where they get cut off)
bool lastWasSane = true;
bool lastSaneWasLowercase = true;
for(int index = 0; index < filenameLength; ++index) {
if(Array.IndexOf(saneCharacters, filename[index]) == -1) {
lastWasSane = false;
} else if(lastWasSane) {
saneFilenameBuilder.Append(filename[index]);
lastSaneWasLowercase = char.IsLower(filename, index);
} else {
if(lastSaneWasLowercase && char.IsUpper(filename, index)) {
lastSaneWasLowercase = false;
} else {
saneFilenameBuilder.Append('-'); // append '-' only between lowercase gaps
}
saneFilenameBuilder.Append(filename[index]);
lastWasSane = true;
}
}
// The sane filename builder should now contain the sane name or nothing.
return saneFilenameBuilder.ToString();
}
}
} // namespace Framework.Storage