437 lines
16 KiB
C#
437 lines
16 KiB
C#
// Copyright (c) 2024 Synty Studios Limited. All rights reserved.
|
|
//
|
|
// Use of this software is subject to the terms and conditions of the Synty Studios End User Licence Agreement (EULA)
|
|
// available at: https://syntystore.com/pages/end-user-licence-agreement
|
|
//
|
|
// For additional details, see the LICENSE.MD file bundled with this software.
|
|
|
|
// using SqlCipher4Unity3D;
|
|
|
|
// using SqlCipher4Unity3D;
|
|
|
|
using SQLite;
|
|
using Synty.SidekickCharacters.Database.DTO;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
|
|
namespace Synty.SidekickCharacters.Database
|
|
{
|
|
/// <summary>
|
|
/// Manages connecting to, initializing and validating a local Sidekick SQLite database
|
|
/// </summary>
|
|
public class DatabaseManager
|
|
{
|
|
private const string _DATABASE_FILE_NAME = "Side_Kick_Data.db";
|
|
private const string _LEGACY_PACKAGE_ROOT = "Assets/Synty/SidekickCharacters";
|
|
private const string _LEGACY_TOOLS_PACKAGE_ROOT = "Assets/Synty/Tools/SidekickCharacters";
|
|
private const string _WORKING_DATABASE_DIRECTORY = "SidekickCharacters";
|
|
private readonly string _CURRENT_VERSION = "1.0.2";
|
|
|
|
private static SQLiteConnection _connection;
|
|
private static int _connectionHash;
|
|
private static string _databasePath;
|
|
private static string _seedDatabasePath;
|
|
private static string _packageRootAbsolutePath;
|
|
private static string _packageRootAssetPath;
|
|
|
|
/// <summary>
|
|
/// Gets the DB connection with the given connection details.
|
|
/// If the current connection is not for the given details, the connection is replaced with the correct connection.
|
|
/// </summary>
|
|
/// <param name="databasePath">The path to the DB.</param>
|
|
/// <param name="connectionKey">The connection key for the DB.</param>
|
|
/// <param name="checkDbOnLoad">Whether or not to valiate the structure of the DB after connecting to it.</param>
|
|
/// <returns>A connection to a DB with the given connection details.</returns>
|
|
public SQLiteConnection GetDbConnection(bool checkDbOnLoad = false)
|
|
{
|
|
if (_connection == null)
|
|
{
|
|
string databasePath = GetDatabasePath();
|
|
if (string.IsNullOrEmpty(databasePath))
|
|
{
|
|
throw new FileNotFoundException("Unable to locate Sidekick database file in this Unity project.");
|
|
}
|
|
|
|
_connection = new SQLiteConnection(databasePath, true);
|
|
}
|
|
else
|
|
{
|
|
return _connection;
|
|
}
|
|
|
|
if (checkDbOnLoad)
|
|
{
|
|
bool createTables = !IsDatabaseConfigured();
|
|
|
|
InitialiseDatabase(createTables);
|
|
}
|
|
|
|
return _connection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whatever the current DB connection is, regardless of state.
|
|
/// </summary>
|
|
/// <returns>The current DB connection.</returns>
|
|
public SQLiteConnection GetCurrentDbConnection()
|
|
{
|
|
return _connection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the current DB connection.
|
|
/// </summary>
|
|
public void CloseConnection()
|
|
{
|
|
if (_connection == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// NOTE: Previously, if we didn't clear the pool in the connection, it resulted in a file lock on the database,
|
|
// which in some cases could cause problems when trying to update the DB to a new version.
|
|
// Our SQLCipher library doesn't use pool clearing unless using SQLiteAsyncConnection.
|
|
_connection.Dispose();
|
|
// Our SQLCipher library doesn't surface checking connection state; disposed connections need their reference removed
|
|
_connection = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialises the Sidekicks database with required data, if they don't already exist.
|
|
/// </summary>
|
|
/// <param name="createTables">Whether to update the database and create the needed tables or not.</param>
|
|
private void InitialiseDatabase(bool createTables = false)
|
|
{
|
|
// ensure we have a default color set (a bunch of other code relies on this, not safe to remove yet)
|
|
try
|
|
{
|
|
SidekickColorSet.GetDefault(this);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
SidekickColorSet newSet = new SidekickColorSet
|
|
{
|
|
Species = new SidekickSpecies { ID = -1, Name = "None" },
|
|
Name = "Default",
|
|
SourceColorPath = GetPackageAssetPath("Resources", "Textures", "T_ColorMap.png"),
|
|
SourceMetallicPath = GetPackageAssetPath("Resources", "Textures", "T_MetallicMap.png"),
|
|
SourceSmoothnessPath = GetPackageAssetPath("Resources", "Textures", "T_SmoothnessMap.png"),
|
|
SourceReflectionPath = GetPackageAssetPath("Resources", "Textures", "T_ReflectionMap.png"),
|
|
SourceEmissionPath = GetPackageAssetPath("Resources", "Textures", "T_EmissionMap.png"),
|
|
SourceOpacityPath = GetPackageAssetPath("Resources", "Textures", "T_OpacityMap.png"),
|
|
};
|
|
|
|
newSet.Save(this);
|
|
}
|
|
|
|
if (createTables)
|
|
{
|
|
GetCurrentDbConnection().CreateTable<SidekickDBVersion>();
|
|
|
|
SidekickDBVersion version;
|
|
|
|
if (GetCurrentDbConnection().Table<SidekickDBVersion>().Any())
|
|
{
|
|
version = GetCurrentDbConnection().Table<SidekickDBVersion>().FirstOrDefault();
|
|
version.SemanticVersion = _CURRENT_VERSION;
|
|
version.LastUpdated = DateTime.Now;
|
|
}
|
|
else
|
|
{
|
|
version = new SidekickDBVersion()
|
|
{
|
|
ID = -1,
|
|
SemanticVersion = _CURRENT_VERSION,
|
|
LastUpdated = DateTime.Now
|
|
};
|
|
}
|
|
|
|
version.Save(this);
|
|
|
|
GetCurrentDbConnection().CreateTable<SidekickPart>();
|
|
GetCurrentDbConnection().CreateTable<SidekickPartImage>();
|
|
GetCurrentDbConnection().CreateTable<SidekickPartFilter>();
|
|
GetCurrentDbConnection().CreateTable<SidekickPartFilterRow>();
|
|
GetCurrentDbConnection().CreateTable<SidekickPartPresetRow>();
|
|
GetCurrentDbConnection().CreateTable<SidekickPartSpeciesLink>();
|
|
GetCurrentDbConnection().CreateTable<SidekickPresetFilter>();
|
|
GetCurrentDbConnection().CreateTable<SidekickPresetFilterRow>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks to see if the current database has the required tables.
|
|
/// TODO: Check DB version, not just if table is present.
|
|
/// </summary>
|
|
/// <returns>True if the tables are present; otherwise false.</returns>
|
|
private bool IsDatabaseConfigured()
|
|
{
|
|
bool configured = !(GetCurrentDbConnection().GetTableInfo("sk_vdata").Count < 1);
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_part").Count < 9)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_part_image").Count < 1)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_part_filter").Count < 1)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_part_filter_row").Count < 1)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_part_preset_row").Count < 5)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_part_species_link").Count < 3)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_preset_filter").Count < 1)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
if (GetCurrentDbConnection().GetTableInfo("sk_preset_filter_row").Count < 1)
|
|
{
|
|
configured = false;
|
|
}
|
|
|
|
return configured;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the current database version.
|
|
/// </summary>
|
|
/// <returns>Semantic version (major.minor.patch).</returns>
|
|
public Version GetDatabaseVersion()
|
|
{
|
|
return new Version(GetCurrentDbConnection()?.Table<SidekickDBVersion>().FirstOrDefault().SemanticVersion ?? "0.0.1ea");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the absolute filesystem path to the Sidekick database file.
|
|
/// </summary>
|
|
/// <returns>The absolute path to the DB file, if found.</returns>
|
|
public static string GetDatabasePath()
|
|
{
|
|
if (!string.IsNullOrEmpty(_databasePath) && File.Exists(_databasePath))
|
|
{
|
|
return _databasePath;
|
|
}
|
|
|
|
string seedDatabasePath = GetSeedDatabasePath();
|
|
if (string.IsNullOrEmpty(seedDatabasePath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
|
|
if (string.IsNullOrEmpty(projectRoot))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string workingDirectory = Path.Combine(projectRoot, "Library", _WORKING_DATABASE_DIRECTORY);
|
|
Directory.CreateDirectory(workingDirectory);
|
|
|
|
_databasePath = Path.Combine(workingDirectory, _DATABASE_FILE_NAME);
|
|
if (!File.Exists(_databasePath))
|
|
{
|
|
File.Copy(seedDatabasePath, _databasePath, false);
|
|
}
|
|
|
|
_databasePath = Path.GetFullPath(_databasePath);
|
|
CleanupLegacyDatabaseArtifacts(seedDatabasePath);
|
|
return _databasePath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the absolute filesystem path to the Sidekick package root.
|
|
/// </summary>
|
|
/// <returns>The absolute path to the package root, if found.</returns>
|
|
public static string GetPackageRootAbsolutePath()
|
|
{
|
|
if (!string.IsNullOrEmpty(_packageRootAbsolutePath) && Directory.Exists(_packageRootAbsolutePath))
|
|
{
|
|
return _packageRootAbsolutePath;
|
|
}
|
|
|
|
string seedDatabasePath = GetSeedDatabasePath();
|
|
if (string.IsNullOrEmpty(seedDatabasePath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string databaseDirectory = Path.GetDirectoryName(seedDatabasePath);
|
|
if (string.IsNullOrEmpty(databaseDirectory))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
_packageRootAbsolutePath = Path.GetDirectoryName(databaseDirectory);
|
|
if (!string.IsNullOrEmpty(_packageRootAbsolutePath))
|
|
{
|
|
_packageRootAbsolutePath = Path.GetFullPath(_packageRootAbsolutePath);
|
|
}
|
|
|
|
return _packageRootAbsolutePath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the project-relative asset path to the Sidekick package root.
|
|
/// </summary>
|
|
/// <returns>The asset path to the package root, if found.</returns>
|
|
public static string GetPackageRootAssetPath()
|
|
{
|
|
if (!string.IsNullOrEmpty(_packageRootAssetPath))
|
|
{
|
|
return _packageRootAssetPath;
|
|
}
|
|
|
|
string packageRootAbsolutePath = GetPackageRootAbsolutePath();
|
|
if (string.IsNullOrEmpty(packageRootAbsolutePath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
_packageRootAssetPath = ToAssetPath(packageRootAbsolutePath);
|
|
return _packageRootAssetPath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a project-relative asset path under the Sidekick package root.
|
|
/// </summary>
|
|
/// <param name="relativeSegments">Path segments under the package root.</param>
|
|
/// <returns>The combined asset path, if the package root could be found.</returns>
|
|
public static string GetPackageAssetPath(params string[] relativeSegments)
|
|
{
|
|
string packageRootAssetPath = GetPackageRootAssetPath();
|
|
if (string.IsNullOrEmpty(packageRootAssetPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string combinedPath = packageRootAssetPath;
|
|
foreach (string segment in relativeSegments)
|
|
{
|
|
combinedPath = Path.Combine(combinedPath, segment);
|
|
}
|
|
|
|
return NormalizePathSeparators(combinedPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalizes legacy Sidekick asset paths so moved packages still load correctly.
|
|
/// </summary>
|
|
/// <param name="assetPath">The asset path to normalize.</param>
|
|
/// <returns>The remapped path if it pointed at an old root; otherwise the original path.</returns>
|
|
public static string NormalizeLegacyAssetPath(string assetPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(assetPath))
|
|
{
|
|
return assetPath;
|
|
}
|
|
|
|
string normalizedPath = NormalizePathSeparators(assetPath);
|
|
string packageRootAssetPath = GetPackageRootAssetPath();
|
|
if (string.IsNullOrEmpty(packageRootAssetPath))
|
|
{
|
|
return normalizedPath;
|
|
}
|
|
|
|
if (normalizedPath.StartsWith(_LEGACY_TOOLS_PACKAGE_ROOT, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return packageRootAssetPath + normalizedPath.Substring(_LEGACY_TOOLS_PACKAGE_ROOT.Length);
|
|
}
|
|
|
|
if (normalizedPath.StartsWith(_LEGACY_PACKAGE_ROOT, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return packageRootAssetPath + normalizedPath.Substring(_LEGACY_PACKAGE_ROOT.Length);
|
|
}
|
|
|
|
return normalizedPath;
|
|
}
|
|
|
|
private static string ToAssetPath(string fullPath)
|
|
{
|
|
string normalizedFullPath = NormalizePathSeparators(Path.GetFullPath(fullPath));
|
|
string normalizedAssetsPath = NormalizePathSeparators(Path.GetFullPath(Application.dataPath));
|
|
|
|
if (!normalizedFullPath.StartsWith(normalizedAssetsPath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return normalizedFullPath;
|
|
}
|
|
|
|
return "Assets" + normalizedFullPath.Substring(normalizedAssetsPath.Length);
|
|
}
|
|
|
|
private static string NormalizePathSeparators(string path)
|
|
{
|
|
return path?.Replace('\\', '/');
|
|
}
|
|
|
|
private static string GetSeedDatabasePath()
|
|
{
|
|
if (!string.IsNullOrEmpty(_seedDatabasePath) && File.Exists(_seedDatabasePath))
|
|
{
|
|
return _seedDatabasePath;
|
|
}
|
|
|
|
string[] databaseFiles = Directory.GetFiles(Application.dataPath, _DATABASE_FILE_NAME, SearchOption.AllDirectories);
|
|
_seedDatabasePath = databaseFiles
|
|
.OrderBy(path => path.Contains($"{Path.DirectorySeparatorChar}SidekickCharacters{Path.DirectorySeparatorChar}Database{Path.DirectorySeparatorChar}") ? 0 : 1)
|
|
.FirstOrDefault();
|
|
|
|
if (!string.IsNullOrEmpty(_seedDatabasePath))
|
|
{
|
|
_seedDatabasePath = Path.GetFullPath(_seedDatabasePath);
|
|
}
|
|
|
|
return _seedDatabasePath;
|
|
}
|
|
|
|
private static void CleanupLegacyDatabaseArtifacts(string seedDatabasePath)
|
|
{
|
|
if (string.IsNullOrEmpty(seedDatabasePath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
DeleteIfExists(seedDatabasePath + "-journal");
|
|
DeleteIfExists(seedDatabasePath + "-journal.meta");
|
|
DeleteIfExists(seedDatabasePath + "-wal");
|
|
DeleteIfExists(seedDatabasePath + "-wal.meta");
|
|
DeleteIfExists(seedDatabasePath + "-shm");
|
|
DeleteIfExists(seedDatabasePath + "-shm.meta");
|
|
}
|
|
|
|
private static void DeleteIfExists(string path)
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|