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