Files
Cielonos/Packages/dev.yarnspinner.unity/Editor/Utility/YarnEditorUtility.cs
SoulliesOfficial 8186f54e90 新场景,剧情
2026-06-02 12:55:39 -04:00

327 lines
14 KiB
C#

/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
#nullable enable
namespace Yarn.Unity.Editor
{
/// <summary>
/// Contains utility methods for working with Yarn Spinner content in
/// the Unity Editor.
/// </summary>
public static class YarnEditorUtility
{
// GUID for editor assets. (Doing it like this means that we don't
// have to worry about where the assets are on disk, if the user
// has moved Yarn Spinner around.)
const string DocumentIconTextureGUID = "0ed312066ea6f40f6af965f21c818b34";
const string ProjectIconTextureGUID = "f6a533d9225cd40ea9ded31d4f686e3b";
const string LocalizationIconTextureGUID = "2cbba4ddd142149b0a38697070990deb";
const string YarnScriptTemplateFileGUID = "4f4ca4a46020a454f80e2ac78eda5aa1";
const string DialoguePresenterTemplateFileGUID = "4a168359cda6140c0bddcd5955a326e4";
/// <summary>
/// Returns a <see cref="Texture2D"/> that can be used to represent
/// Yarn files.
/// </summary>
/// <returns>A texture to use in the Unity editor for Yarn
/// files.</returns>
public static Texture2D GetYarnDocumentIconTexture()
{
string textureAssetPath = AssetDatabase.GUIDToAssetPath(DocumentIconTextureGUID);
return AssetDatabase.LoadAssetAtPath<Texture2D>(textureAssetPath);
}
/// <summary>
/// Returns a <see cref="Texture2D"/> that can be used to represent
/// Yarn project files.
/// </summary>
/// <returns>A texture to use in the Unity editor for Yarn project
/// files.</returns>
public static Texture2D GetYarnProjectIconTexture()
{
string textureAssetPath = AssetDatabase.GUIDToAssetPath(ProjectIconTextureGUID);
return AssetDatabase.LoadAssetAtPath<Texture2D>(textureAssetPath);
}
/// <summary>
/// Returns a <see cref="Texture2D"/> that can be used to represent
/// built-in Localization objects.
/// </summary>
/// <returns>A texture to use in the Unity editor for Yarn built-in
/// Localization files.</returns>
public static Texture2D GetLocalizationIconTexture()
{
string textureAssetPath = AssetDatabase.GUIDToAssetPath(LocalizationIconTextureGUID);
return AssetDatabase.LoadAssetAtPath<Texture2D>(textureAssetPath);
}
/// <summary>
/// Returns the path to a text file that can be used as the basis
/// for newly created Yarn scripts.
/// </summary>
/// <returns>A path to a file to use in the Unity editor for
/// creating new Yarn scripts.</returns>
/// <throws cref="FileNotFoundException">Thrown if the template
/// text file cannot be found.</throws>
public static string GetTemplateYarnScriptPath()
{
var path = AssetDatabase.GUIDToAssetPath(YarnScriptTemplateFileGUID);
if (string.IsNullOrEmpty(path))
{
throw new System.IO.FileNotFoundException($"Template file for new Yarn scripts couldn't be found. Have the .meta files for Yarn Spinner been modified or deleted? Try re-importing the Yarn Spinner package to fix this error.");
}
return path;
}
/// <summary>
/// Returns the path to a text file that can be used as the basis
/// for newly created C# Dialogue Presenter scripts.
/// </summary>
/// <returns>A path to a file to use in the Unity editor for
/// creating new C# Dialogue Presenter.</returns>
/// <throws cref="FileNotFoundException">Thrown if the template
/// text file cannot be found.</throws>
public static string GetTemplateDialoguePresenterPath()
{
var path = AssetDatabase.GUIDToAssetPath(DialoguePresenterTemplateFileGUID);
if (string.IsNullOrEmpty(path))
{
throw new System.IO.FileNotFoundException($"Template file for Dialogue View scripts couldn't be found. Have the .meta files for Yarn Spinner been modified or deleted? Try re-importing the Yarn Spinner package to fix this error.");
}
return path;
}
/// <summary>
/// Begins the interactive process of creating a new Yarn file in
/// the Editor.
/// </summary>
[MenuItem("Assets/Create/Yarn Spinner/Yarn Script", false, 10)]
public static void CreateYarnAsset()
{
// This method call is undocumented, but public. It's defined
// in ProjectWindowUtil, and used by other parts of the editor
// to create other kinds of assets (scripts, textures, etc).
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(
default,
ScriptableObject.CreateInstance<DoCreateYarnScriptAsset>(),
"NewYarnScript.yarn",
GetYarnDocumentIconTexture(),
GetTemplateYarnScriptPath());
}
/// <summary>
/// Creates a new Yarn Project asset in the current folder, and begins
/// interactively renaming it.
/// </summary>
[MenuItem("Assets/Create/Yarn Spinner/Yarn Project", false, 101)]
public static void CreateYarnProject()
{
// This method call is undocumented, but public. It's defined
// in ProjectWindowUtil, and used by other parts of the editor
// to create other kinds of assets (scripts, textures, etc).
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(
default,
ScriptableObject.CreateInstance<DoCreateYarnProjectAsset>(),
"NewProject.yarnproject",
GetYarnProjectIconTexture(),
GetTemplateYarnScriptPath());
}
/// <summary>
/// Creates a new C# script asset containing a template Dialogue Presenter in
/// the current folder, and begins interactively renaming it.
/// </summary>
[MenuItem("Assets/Create/Yarn Spinner/Dialogue Presenter Script", false, 111)]
[MenuItem("Assets/Create/Scripting/Yarn Spinner/Dialogue Presenter Script", false, 101)]
public static void CreateDialoguePresenterScript()
{
// This method call is undocumented, but public. It's defined
// in ProjectWindowUtil, and used by other parts of the editor
// to create other kinds of assets (scripts, textures, etc).
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(
default,
ScriptableObject.CreateInstance<DoCreateYarnScriptAsset>(),
"NewDialoguePresenter.cs",
null,
GetTemplateDialoguePresenterPath());
}
/// <summary>
/// Writes a Yarn Project to <paramref name="path"/>.
/// </summary>
/// <param name="path">The path at which to write the file.</param>
/// <param name="project">The Yarn Project to write to disk.</param>
public static Object CreateYarnProject(string path, Compiler.Project project)
{
var text = project.GetJson();
File.WriteAllText(path, text);
AssetDatabase.ImportAsset(path);
return AssetDatabase.LoadAssetAtPath<Object>(path);
}
/// <summary>
/// Creates a new Yarn script at the given path, using the default
/// template.
/// </summary>
/// <param name="path">The path at which to create the
/// script.</param>
public static Object CreateYarnAsset(string path)
{
return CreateScriptAssetFromTemplate(path, GetTemplateYarnScriptPath());
}
private static Object CreateScriptAssetFromTemplate(string pathName, string resourceFile)
{
// Read the contents of the template file
string templateContent;
try
{
templateContent = File.ReadAllText(resourceFile);
}
catch
{
Debug.LogError("Failed to find template file. Creating an empty file instead.");
templateContent = "";
}
// The file name is the name of the file, sans extension.
string fileName = Path.GetFileNameWithoutExtension(pathName);
// Replace any spaces with underscores - these aren't allowed
// in node names
fileName = fileName.Replace(" ", "_");
// Replace the placeholder with the file name
templateContent = templateContent.Replace("#SCRIPTNAME#", fileName);
// Respect the user's line endings preferences for this new
// text asset
string unixLineEndings = "\n";
string windowsLineEndings = "\r\n";
string lineEndings;
switch (EditorSettings.lineEndingsForNewScripts)
{
case LineEndingsMode.OSNative:
// OS native = use Windows if we're on Windows, else
// Unix
var isWindows = Application.platform == RuntimePlatform.WindowsEditor;
lineEndings = isWindows ? windowsLineEndings : unixLineEndings;
break;
case LineEndingsMode.Windows:
// Windows = use Windows endings
lineEndings = windowsLineEndings;
break;
case LineEndingsMode.Unix:
default:
// Unix or anything else = use Unix endings
lineEndings = unixLineEndings;
break;
}
// Replace every line ending in the template (this way we don't
// need to keep track of which line ending the asset was last
// saved in)
templateContent = System.Text.RegularExpressions.Regex.Replace(templateContent, @"\r\n?|\n", lineEndings);
// Write it all out to disk as UTF-8
var fullPath = Path.GetFullPath(pathName);
File.WriteAllText(fullPath, templateContent, System.Text.Encoding.UTF8);
// Force Unity to notice the new asset (this will also compile
// the new, empty Yarn script)
AssetDatabase.ImportAsset(pathName);
// We don't hugely care about the details of the object anyway
// (we just wanted to ensure that it's imported as at least an
// asset), so we'll return it as an Object here.
return AssetDatabase.LoadAssetAtPath<Object>(pathName);
}
private static void CreateYarnScriptAsset(string pathName, string resourceFile)
{
// Produce the asset.
Object o = CreateScriptAssetFromTemplate(pathName, resourceFile);
// Reveal it on disk.
ProjectWindowUtil.ShowCreatedAsset(o);
}
private static void CreateYarnProjectAsset(string pathName)
{
// Produce the asset.
var project = YarnProjectUtility.CreateDefaultYarnProject();
var json = project.GetJson();
// Write it all out to disk as UTF-8
var fullPath = Path.GetFullPath(pathName);
File.WriteAllText(fullPath, json, System.Text.Encoding.UTF8);
// Force Unity to notice the new asset.
AssetDatabase.ImportAsset(pathName);
Object o = AssetDatabase.LoadAssetAtPath<Object>(pathName);
// Reveal it on disk.
ProjectWindowUtil.ShowCreatedAsset(o);
}
// A handler that receives a callback after the user finishes
// naming a new file.
#if UNITY_6000_4_OR_NEWER
private class DoCreateYarnScriptAsset : UnityEditor.ProjectWindowCallback.AssetCreationEndAction
{
public override void Action(EntityId entityId, string pathName, string resourceFile) => CreateYarnScriptAsset(pathName, resourceFile);
}
#else
private class DoCreateYarnScriptAsset : UnityEditor.ProjectWindowCallback.EndNameEditAction
{
public override void Action(int instanceId, string pathName, string resourceFile) => CreateYarnScriptAsset(pathName, resourceFile);
}
#endif
// A handler that receives a callback after the user finishes naming a
// new file. The user just finished typing (and didn't cancel it by
// pressing escape or anything.) Commit the action by generating the
// file on disk.
#if UNITY_6000_4_OR_NEWER
private class DoCreateYarnProjectAsset : UnityEditor.ProjectWindowCallback.AssetCreationEndAction
{
public override void Action(EntityId entityId, string pathName, string resourceFile) => CreateYarnProjectAsset(pathName);
}
#else
private class DoCreateYarnProjectAsset : UnityEditor.ProjectWindowCallback.EndNameEditAction
{
public override void Action(int instanceId, string pathName, string resourceFile) => CreateYarnProjectAsset(pathName);
}
#endif
/// <summary>
/// Get all assets of a given type.
/// </summary>
/// <typeparam name="T">AssetImporter type to search for. Should be convertible from AssetImporter.</typeparam>
/// <param name="filterQuery">Asset query (see <see cref="AssetDatabase.FindAssets(string)"/> documentation for formatting).</param>
/// <param name="converter">Custom type caster.</param>
/// <returns>Enumerable of all assets of a given type.</returns>
public static IEnumerable<T> GetAllAssetsOf<T>(string filterQuery, System.Func<AssetImporter, T>? converter = null) where T : class
=> AssetDatabase.FindAssets(filterQuery)
.Select(AssetDatabase.GUIDToAssetPath)
.Select(AssetImporter.GetAtPath)
.Select(importer => converter?.Invoke(importer) ?? importer as T)
.Where(source => source != null)!;
}
}