From c66c91e8e9e12083374a12332da95398835bd39b Mon Sep 17 00:00:00 2001 From: dal4segno Date: Sun, 22 Mar 2026 23:50:10 +0900 Subject: [PATCH] Fix Sidekick tool startup and path handling --- .../Scripts/Editor/ModularCharacterWindow.cs | 19 +- .../Scripts/Editor/Utility/ToolDownloader.cs | 50 +++- .../Scripts/Runtime/API/SidekickRuntime.cs | 40 ++- .../Runtime/Database/DTO/SidekickColorSet.cs | 12 + .../Runtime/Database/DTO/SidekickPart.cs | 2 + .../Runtime/Database/DatabaseManager.cs | 242 +++++++++++++++++- 6 files changed, 345 insertions(+), 20 deletions(-) diff --git a/Assets/External/Models/SidekickCharacters/Scripts/Editor/ModularCharacterWindow.cs b/Assets/External/Models/SidekickCharacters/Scripts/Editor/ModularCharacterWindow.cs index 626ed7a7..41785c2a 100644 --- a/Assets/External/Models/SidekickCharacters/Scripts/Editor/ModularCharacterWindow.cs +++ b/Assets/External/Models/SidekickCharacters/Scripts/Editor/ModularCharacterWindow.cs @@ -40,7 +40,7 @@ namespace Synty.SidekickCharacters private const string _AUTOSAVE_MISSING_PARTS = "SK_Autosave_missing_parts"; private const string _AUTOSAVE_STATE = "SK_Autosave_state"; private const string _BASE_COLOR_SET_NAME = "Species"; - private const string _BASE_COLOR_SET_PATH = "Assets/Synty/SidekickCharacters/Resources/Species"; + private const string _BASE_COLOR_SET_PATH = "Assets/External/Models/SidekickCharacters/Resources/Species"; private const string _BASE_MESH_NAME = "Meshes/SK_BaseModel"; private const string _BASE_MATERIAL_NAME = "Materials/M_BaseMaterial"; private const string _BLEND_GENDER_NAME = "masculineFeminine"; @@ -121,7 +121,7 @@ namespace Synty.SidekickCharacters private bool _showAllColourProperties = false; private float _bodyTypeBlendValue; private bool _loadingCharacter = false; - private bool _loadingContent = true; + private bool _loadingContent = false; private Image _loadingImage; private ObjectField _materialField; private float _musclesBlendValue; @@ -192,7 +192,7 @@ namespace Synty.SidekickCharacters } // ensures we release the file lock on the database - _dbManager.CloseConnection(); + _dbManager?.CloseConnection(); } #if UNITY_EDITOR @@ -211,7 +211,7 @@ namespace Synty.SidekickCharacters /// private void Update() { - if (_loadingContent) + if (_loadingContent && _loadingImage != null) { Vector3 rotation = _loadingImage.transform.rotation.eulerAngles; rotation += new Vector3(0, 0, 0.5f * Time.deltaTime); @@ -473,6 +473,7 @@ namespace Synty.SidekickCharacters // if we still can't connect, something's gone wrong, don't keep building the GUI if (_dbManager?.GetCurrentDbConnection() == null) { + _loadingContent = false; return; } @@ -1814,7 +1815,12 @@ namespace Synty.SidekickCharacters documentationButton.clickable.clicked += delegate { - Application.OpenURL("file:Assets/Synty/SidekickCharacters/Documentation/SidekickCharacters_UserGuide.pdf"); + string documentationPath = Path.Combine(DatabaseManager.GetPackageRootAbsolutePath() ?? string.Empty, "Documentation", "SidekickCharacters_UserGuide.pdf"); + documentationPath = Path.GetFullPath(documentationPath); + if (File.Exists(documentationPath)) + { + Application.OpenURL("file:" + documentationPath); + } }; Button storeButton = new Button @@ -1919,7 +1925,8 @@ namespace Synty.SidekickCharacters /// private void SaveColorSet() { - string path = Path.Combine(_BASE_COLOR_SET_PATH, _currentSpecies.Name); + string baseColorSetPath = DatabaseManager.GetPackageAssetPath("Resources", "Species") ?? _BASE_COLOR_SET_PATH; + string path = Path.Combine(baseColorSetPath, _currentSpecies.Name); path = Path.Combine(path, _currentColorSet.Name.Replace(" ", "_")); SaveTexturesToDisk(path); diff --git a/Assets/External/Models/SidekickCharacters/Scripts/Editor/Utility/ToolDownloader.cs b/Assets/External/Models/SidekickCharacters/Scripts/Editor/Utility/ToolDownloader.cs index 93a04d17..c162b780 100644 --- a/Assets/External/Models/SidekickCharacters/Scripts/Editor/Utility/ToolDownloader.cs +++ b/Assets/External/Models/SidekickCharacters/Scripts/Editor/Utility/ToolDownloader.cs @@ -24,7 +24,7 @@ namespace Synty.SidekickCharacters private const string _GIT_DB_URL = "https://github.com/SyntyStudios/SidekicksToolRelease/releases/latest/download/SidekicksDatabase.unitypackage"; private const string _PACKAGE_CACHE = "Assets/DownloadCache/Sidekicks.unitypackage"; private const string _DB_PACKAGE_CACHE = "Assets/DownloadCache/SidekicksDatabase.unitypackage"; - private const string _VERSION_FILE = "Assets/Synty/SidekickCharacters/Scripts/Editor/version.txt"; + private const string _VERSION_FILE_NAME = "version.txt"; private const string _VERSION_TAG = "\"tag_name\":"; private const string _VERSION_KEY = "sk_current_tool_version"; private const string _SIDEKICK_TOOL_MENU_ITEM = "Synty/Sidekick Character Tool"; @@ -168,19 +168,61 @@ namespace Synty.SidekickCharacters private void SaveCurrentInstalledVersion(string version) { - File.WriteAllText(_VERSION_FILE, version); + string versionFilePath = GetVersionFilePath(); + if (string.IsNullOrEmpty(versionFilePath)) + { + Debug.LogWarning("Unable to resolve Sidekick version file path."); + return; + } + + File.WriteAllText(versionFilePath, version); } private string LoadCurrentInstalledVersion() { - if (File.Exists(_VERSION_FILE)) + string versionFilePath = GetVersionFilePath(); + if (string.IsNullOrEmpty(versionFilePath)) { - return File.ReadAllText(_VERSION_FILE); + return null; + } + + if (File.Exists(versionFilePath)) + { + return File.ReadAllText(versionFilePath); } return null; } + private string GetVersionFilePath() + { + MonoScript currentScript = MonoScript.FromScriptableObject(this); + if (currentScript == null) + { + return null; + } + + string scriptPath = AssetDatabase.GetAssetPath(currentScript); + if (string.IsNullOrEmpty(scriptPath)) + { + return null; + } + + string utilityDirectory = Path.GetDirectoryName(scriptPath); + if (string.IsNullOrEmpty(utilityDirectory)) + { + return null; + } + + string editorDirectory = Path.GetDirectoryName(utilityDirectory); + if (string.IsNullOrEmpty(editorDirectory)) + { + return null; + } + + return Path.GetFullPath(Path.Combine(editorDirectory, _VERSION_FILE_NAME)); + } + private void DownloadLatestDBVersion() { WebClient client = new WebClient(); diff --git a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/API/SidekickRuntime.cs b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/API/SidekickRuntime.cs index 52b15710..d38b4db0 100644 --- a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/API/SidekickRuntime.cs +++ b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/API/SidekickRuntime.cs @@ -230,6 +230,42 @@ namespace Synty.SidekickCharacters.API await runtime.PopulatePresetLibrary(); } + private static List GetSidekickPartFiles() + { + string packageRootPath = DatabaseManager.GetPackageRootAbsolutePath(); + if (!string.IsNullOrEmpty(packageRootPath)) + { + string meshesRootPath = Path.Combine(packageRootPath, "Resources", "Meshes"); + if (Directory.Exists(meshesRootPath)) + { + return Directory + .GetFiles(meshesRootPath, "SK_*_*_*_*_*.fbx", SearchOption.AllDirectories) + .Select(ToAssetPath) + .ToList(); + } + } + + return Directory.GetFiles("Assets", "SK_*_*_*_*_*.fbx", SearchOption.AllDirectories).ToList(); + } + + private static string ToAssetPath(string fullPath) + { + if (string.IsNullOrWhiteSpace(fullPath)) + { + return fullPath; + } + + string normalizedFullPath = fullPath.Replace('\\', '/'); + string normalizedAssetsPath = Application.dataPath.Replace('\\', '/'); + + if (!normalizedFullPath.StartsWith(normalizedAssetsPath, StringComparison.OrdinalIgnoreCase)) + { + return normalizedFullPath; + } + + return "Assets" + normalizedFullPath.Substring(normalizedAssetsPath.Length); + } + /// /// Takes all the parts selected in the window, and combines them into a single model in the scene. /// @@ -512,7 +548,7 @@ namespace Synty.SidekickCharacters.API _partOutfitToggleMap = new Dictionary(); _partCount = 0; - List files = Directory.GetFiles("Assets", "SK_*_*_*_*_*.fbx", SearchOption.AllDirectories).ToList(); + List files = GetSidekickPartFiles(); foreach (CharacterPartType partType in Enum.GetValues(typeof(CharacterPartType))) { @@ -610,7 +646,7 @@ namespace Synty.SidekickCharacters.API _speciesDictionary = new Dictionary(); _partCount = 0; - List files = Directory.GetFiles("Assets", "SK_*_*_*_*_*.fbx", SearchOption.AllDirectories).ToList(); + List files = GetSidekickPartFiles(); Dictionary filesOnDisk = new Dictionary(); foreach (string file in files) { diff --git a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickColorSet.cs b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickColorSet.cs index 696956ae..02461545 100644 --- a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickColorSet.cs +++ b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickColorSet.cs @@ -154,6 +154,18 @@ namespace Synty.SidekickCharacters.Database.DTO /// A color set with all DTO class properties set private static void Decorate(DatabaseManager dbManager, SidekickColorSet set) { + if (set == null) + { + return; + } + + set.SourceColorPath = DatabaseManager.NormalizeLegacyAssetPath(set.SourceColorPath); + set.SourceMetallicPath = DatabaseManager.NormalizeLegacyAssetPath(set.SourceMetallicPath); + set.SourceSmoothnessPath = DatabaseManager.NormalizeLegacyAssetPath(set.SourceSmoothnessPath); + set.SourceReflectionPath = DatabaseManager.NormalizeLegacyAssetPath(set.SourceReflectionPath); + set.SourceEmissionPath = DatabaseManager.NormalizeLegacyAssetPath(set.SourceEmissionPath); + set.SourceOpacityPath = DatabaseManager.NormalizeLegacyAssetPath(set.SourceOpacityPath); + if (set.Species == null && set.PtrSpecies >= 0) { set.Species = SidekickSpecies.GetByID(dbManager, set.PtrSpecies); diff --git a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickPart.cs b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickPart.cs index fd2fdf2b..d0ad6246 100644 --- a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickPart.cs +++ b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DTO/SidekickPart.cs @@ -222,6 +222,8 @@ namespace Synty.SidekickCharacters.Database.DTO { if (part != null) { + part.Location = DatabaseManager.NormalizeLegacyAssetPath(part.Location); + if (part.Species == null && part.PtrSpecies >= 0) { part.Species = SidekickSpecies.GetByID(dbManager, part.PtrSpecies); diff --git a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DatabaseManager.cs b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DatabaseManager.cs index 80378c09..36ae79e6 100644 --- a/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DatabaseManager.cs +++ b/Assets/External/Models/SidekickCharacters/Scripts/Runtime/Database/DatabaseManager.cs @@ -12,7 +12,9 @@ using SQLite; using Synty.SidekickCharacters.Database.DTO; using System; +using System.IO; using System.Linq; +using UnityEngine; namespace Synty.SidekickCharacters.Database { @@ -21,11 +23,18 @@ namespace Synty.SidekickCharacters.Database /// public class DatabaseManager { - private static readonly string _DATABASE_PATH = "Assets/Synty/SidekickCharacters/Database/Side_Kick_Data.db"; + 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; /// /// Gets the DB connection with the given connection details. @@ -39,7 +48,13 @@ namespace Synty.SidekickCharacters.Database { if (_connection == null) { - _connection = new SQLiteConnection(_DATABASE_PATH, true); + 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 { @@ -100,12 +115,12 @@ namespace Synty.SidekickCharacters.Database { Species = new SidekickSpecies { ID = -1, Name = "None" }, Name = "Default", - SourceColorPath = "Assets/Synty/Tools/SidekickCharacters/Resources/Textures/T_ColorMap.png", - SourceMetallicPath = "Assets/Synty/Tools/SidekickCharacters/Resources/Textures/T_MetallicMap.png", - SourceSmoothnessPath = "Assets/Synty/Tools/SidekickCharacters/Resources/Textures/T_SmoothnessMap.png", - SourceReflectionPath = "Assets/Synty/Tools/SidekickCharacters/Resources/Textures/T_ReflectionMap.png", - SourceEmissionPath = "Assets/Synty/Tools/SidekickCharacters/Resources/Textures/T_EmissionMap.png", - SourceOpacityPath = "Assets/Synty/Tools/SidekickCharacters/Resources/Textures/T_OpacityMap.png", + 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); @@ -206,5 +221,216 @@ namespace Synty.SidekickCharacters.Database { return new Version(GetCurrentDbConnection()?.Table().FirstOrDefault().SemanticVersion ?? "0.0.1ea"); } + + /// + /// Gets the absolute filesystem path to the Sidekick database file. + /// + /// The absolute path to the DB file, if found. + 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; + } + + /// + /// Gets the absolute filesystem path to the Sidekick package root. + /// + /// The absolute path to the package root, if found. + 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; + } + + /// + /// Gets the project-relative asset path to the Sidekick package root. + /// + /// The asset path to the package root, if found. + public static string GetPackageRootAssetPath() + { + if (!string.IsNullOrEmpty(_packageRootAssetPath)) + { + return _packageRootAssetPath; + } + + string packageRootAbsolutePath = GetPackageRootAbsolutePath(); + if (string.IsNullOrEmpty(packageRootAbsolutePath)) + { + return null; + } + + _packageRootAssetPath = ToAssetPath(packageRootAbsolutePath); + return _packageRootAssetPath; + } + + /// + /// Builds a project-relative asset path under the Sidekick package root. + /// + /// Path segments under the package root. + /// The combined asset path, if the package root could be found. + 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); + } + + /// + /// Normalizes legacy Sidekick asset paths so moved packages still load correctly. + /// + /// The asset path to normalize. + /// The remapped path if it pointed at an old root; otherwise the original path. + 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 + { + } + } } }