同步
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
internal static class AssetStoreSamplesInstaller
|
||||
{
|
||||
internal const string assetStoreURL = "https://assetstore.unity.com/packages/slug/319418";
|
||||
internal static void InstallSamples()
|
||||
{
|
||||
if (YarnPackageImporter.IsSamplesPackageInstalled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UnityEngine.Application.OpenURL(assetStoreURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd9cd1021d3794a8bb88cc658221e712
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
44
Packages/dev.yarnspinner.unity/Editor/Utility/Compare.cs
Normal file
44
Packages/dev.yarnspinner.unity/Editor/Utility/Compare.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
// A simple class lets us use a delegate as an IEqualityComparer from
|
||||
// https://stackoverflow.com/a/4607559
|
||||
internal static class Compare
|
||||
{
|
||||
public static IEqualityComparer<T> By<T>(System.Func<T, T, bool> comparison)
|
||||
{
|
||||
return new DelegateComparer<T>(comparison);
|
||||
}
|
||||
|
||||
private class DelegateComparer<T> : EqualityComparer<T>
|
||||
{
|
||||
private readonly System.Func<T, T, bool> comparison;
|
||||
|
||||
public DelegateComparer(System.Func<T, T, bool> identitySelector)
|
||||
{
|
||||
this.comparison = identitySelector;
|
||||
}
|
||||
|
||||
public override bool Equals(T x, T y)
|
||||
{
|
||||
return comparison(x, y);
|
||||
}
|
||||
|
||||
public override int GetHashCode(T obj)
|
||||
{
|
||||
// Force LINQ to never refer to the hash of an object by
|
||||
// returning a constant for all values. This is inefficient
|
||||
// because LINQ can't use an internal comparator, but we're
|
||||
// already looking to use a delegate to do a more
|
||||
// fine-grained test anyway, so we want to ensure that it's
|
||||
// called.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff913f9ce0558401e853febebd2ebc0a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
|
||||
*/
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using Yarn.Unity;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a menu item to the menu bar for creating an instance of the
|
||||
/// dialogue runner prefab.
|
||||
/// </summary>
|
||||
public static class CreateDialogueRunnerMenuItem
|
||||
{
|
||||
const string DialogueRunnerPrefabGUID = "52571f68872914e24837210513edea1d";
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the Dialogue System prefab in the currently active scene,
|
||||
/// and returns the created <see cref="DialogueRunner"/>.
|
||||
/// </summary>
|
||||
/// <returns>A newly created <see cref="DialogueRunner"/>.</returns>
|
||||
/// <exception cref="System.InvalidOperationException">Thrown when the
|
||||
/// Dialogue System prefab cannot be found in the Yarn Spinner
|
||||
/// package.</exception>
|
||||
[MenuItem("GameObject/Yarn Spinner/Dialogue System", priority = 11)]
|
||||
public static DialogueRunner CreateDialogueRunner()
|
||||
{
|
||||
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(DialogueRunnerPrefabGUID);
|
||||
var prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
|
||||
|
||||
if (prefabAsset == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
$"Can't create a new Dialogue System: Can't find the prefab to create a Dialogue System from."
|
||||
);
|
||||
}
|
||||
|
||||
var instantiatedPrefab = (GameObject)PrefabUtility.InstantiatePrefab(prefabAsset);
|
||||
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
var eventSystems = Object.FindObjectsByType<EventSystem>(FindObjectsSortMode.None);
|
||||
#else
|
||||
var eventSystems = Object.FindObjectsOfType<EventSystem>();
|
||||
#endif
|
||||
|
||||
if (eventSystems.Length > 1)
|
||||
{
|
||||
// At least one other event system is present in the scene. Turn off
|
||||
// the one that came with the prefab - it's not needed.
|
||||
var instantiatedEventSystem = instantiatedPrefab.GetComponentInChildren<EventSystem>();
|
||||
|
||||
instantiatedEventSystem.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
return instantiatedPrefab.GetComponent<DialogueRunner>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82e9906f6762e445d8082f495e7c1061
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,186 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
#if USE_UNITY_LOCALIZATION
|
||||
using UnityEngine.Localization;
|
||||
#endif
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
public abstract class PackageSetupStep
|
||||
{
|
||||
public abstract string PerformStepButtonLabel { get; }
|
||||
public abstract string Description { get; }
|
||||
public abstract bool NeedsSetup { get; }
|
||||
public abstract void RunSetup();
|
||||
}
|
||||
|
||||
public class CustomPackageSetupStep : PackageSetupStep
|
||||
{
|
||||
public override string Description { get; }
|
||||
public override string PerformStepButtonLabel { get; }
|
||||
public override bool NeedsSetup => this.NeedsSetupAction();
|
||||
public override void RunSetup() => this.RunSetupAction();
|
||||
|
||||
private System.Func<bool> NeedsSetupAction { get; }
|
||||
private System.Action RunSetupAction { get; }
|
||||
|
||||
public CustomPackageSetupStep(string description,
|
||||
string performStepButtonLabel,
|
||||
System.Func<bool> needsSetup,
|
||||
System.Action runSetup)
|
||||
{
|
||||
Description = description;
|
||||
NeedsSetupAction = needsSetup;
|
||||
RunSetupAction = runSetup;
|
||||
PerformStepButtonLabel = performStepButtonLabel;
|
||||
}
|
||||
}
|
||||
|
||||
#if USE_UNITY_LOCALIZATION
|
||||
public class UnityLocalizationSetupStep : PackageSetupStep
|
||||
{
|
||||
public static IEnumerable<string> SampleLocaleIdentifiers => new[] { "en", "es", "pt-BR", "de", "zh-Hans" };
|
||||
public static IDictionary<string, string> SampleLocaleFallbacks => new Dictionary<string, string> { { "es", "en" } };
|
||||
|
||||
public override string Description =>
|
||||
"Unity Localization is installed, but your project doesn't have a " +
|
||||
"Localization Settings asset, and/or it lacks Locale assets that this sample needs.";
|
||||
|
||||
public override string PerformStepButtonLabel => "Create Localization Assets";
|
||||
|
||||
public override bool NeedsSetup
|
||||
{
|
||||
get
|
||||
{
|
||||
// Do we have settings?
|
||||
var settings = UnityEditor.Localization.LocalizationEditorSettings.ActiveLocalizationSettings;
|
||||
if (settings == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Do we have the appropriate locales?
|
||||
foreach (var identifier in SampleLocaleIdentifiers)
|
||||
{
|
||||
// we now have a valid settings, but we don't know if it
|
||||
// has english locale support
|
||||
var localeID = new UnityEngine.Localization.LocaleIdentifier(identifier);
|
||||
if (UnityEngine.Localization.Settings.LocalizationSettings.AvailableLocales.GetLocale(localeID) == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string DestinationPath { get; }
|
||||
|
||||
public UnityLocalizationSetupStep(string destinationPath = "Assets/Localization")
|
||||
{
|
||||
this.DestinationPath = destinationPath;
|
||||
}
|
||||
|
||||
public override void RunSetup()
|
||||
{
|
||||
// First, we need to make sure the folder we're working in exists
|
||||
if (Directory.Exists(this.DestinationPath) == false)
|
||||
{
|
||||
var pieces = this.DestinationPath.Split('/', System.StringSplitOptions.RemoveEmptyEntries);
|
||||
var parent = string.Join('/', pieces.Take(pieces.Length - 1));
|
||||
AssetDatabase.CreateFolder(parent, pieces.Last());
|
||||
}
|
||||
|
||||
// Do we already have a LocalizationSettings asset? If not, create
|
||||
// one and set it up.
|
||||
var settings = UnityEditor.Localization.LocalizationEditorSettings.ActiveLocalizationSettings;
|
||||
if (settings == null)
|
||||
{
|
||||
// Create localization settings
|
||||
settings = ScriptableObject.CreateInstance<UnityEngine.Localization.Settings.LocalizationSettings>();
|
||||
settings.name = "Test Localization Settings";
|
||||
AssetDatabase.CreateAsset(settings, DestinationPath + "/Localization Settings.asset");
|
||||
|
||||
// setting this new settings object to be the global settings
|
||||
// for the project
|
||||
UnityEditor.Localization.LocalizationEditorSettings.ActiveLocalizationSettings = settings;
|
||||
}
|
||||
|
||||
foreach (var identifier in SampleLocaleIdentifiers)
|
||||
{
|
||||
// we now have a valid settings, but we don't know if it has
|
||||
// the locales we need
|
||||
var localeID = new LocaleIdentifier(identifier);
|
||||
if (UnityEngine.Localization.Settings.LocalizationSettings.AvailableLocales.GetLocale(localeID) == null)
|
||||
{
|
||||
// we need to make the asset and add it to the settings
|
||||
// and on disk
|
||||
var locale = Locale.CreateLocale(localeID);
|
||||
AssetDatabase.CreateAsset(locale, DestinationPath + "/Locale " + identifier + ".asset");
|
||||
|
||||
UnityEditor.Localization.LocalizationEditorSettings.AddLocale(locale);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, ensure that the locales have their fallbacks
|
||||
// configured correctly
|
||||
foreach (var (fromLocaleID, toLocaleID) in SampleLocaleFallbacks)
|
||||
{
|
||||
var fromLocale = UnityEditor.Localization.LocalizationEditorSettings.GetLocale(fromLocaleID);
|
||||
var toLocale = UnityEditor.Localization.LocalizationEditorSettings.GetLocale(toLocaleID);
|
||||
|
||||
var fallbackMetadata = fromLocale.Metadata.GetMetadata<UnityEngine.Localization.Metadata.FallbackLocale>();
|
||||
if (fallbackMetadata == null)
|
||||
{
|
||||
fallbackMetadata = new UnityEngine.Localization.Metadata.FallbackLocale();
|
||||
fromLocale.Metadata.AddMetadata(fallbackMetadata);
|
||||
}
|
||||
fallbackMetadata.Locale = toLocale;
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
|
||||
// Find all table collections, and make sure they (and their
|
||||
// contents) are known to the addressable system (which might
|
||||
// have just been installed, so no assets have any addresses)
|
||||
var allTableCollectionGUIDs = AssetDatabase.FindAssets("t:LocalizationTableCollection");
|
||||
|
||||
foreach (var guid in allTableCollectionGUIDs)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var localizationCollection = AssetDatabase.LoadAssetAtPath<UnityEditor.Localization.LocalizationTableCollection>(path);
|
||||
|
||||
// Make sure the table collection's assets are all
|
||||
// addressable
|
||||
|
||||
localizationCollection.RefreshAddressables();
|
||||
|
||||
// If this is an asset table collection, make sure that
|
||||
// every asset in all of its tables is addressable
|
||||
if (localizationCollection is UnityEditor.Localization.AssetTableCollection assetTableCollection)
|
||||
{
|
||||
foreach (var table in assetTableCollection.AssetTables)
|
||||
{
|
||||
var allEntries = new Dictionary<long, UnityEngine.Localization.Tables.AssetTableEntry>(table);
|
||||
|
||||
foreach (var entry in allEntries)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(entry.Value.Guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
|
||||
assetTableCollection.AddAssetToTable(table, entry.Key, asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91c165b2af7cd4bdcbf269890f2a49c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(InterfaceContainer<>), true)]
|
||||
public class InterfaceContainerDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var targetProp = property.FindPropertyRelative(nameof(InterfaceContainer<UnityEngine.Object>.targetObject));
|
||||
EditorGUI.ObjectField(position, targetProp, new UnityEngine.GUIContent(property.displayName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ea37ee3989123c4c88a0c4a218d74bc
|
||||
@@ -0,0 +1,17 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
internal static class ItchSamplesInstaller
|
||||
{
|
||||
internal const string itchURL = "https://yarnspinner.itch.io/yarn-spinner";
|
||||
internal static void InstallSamples()
|
||||
{
|
||||
if (YarnPackageImporter.IsSamplesPackageInstalled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UnityEngine.Application.OpenURL(itchURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c21c80267e2f4204b7d5e261e616faa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
internal static class ManualSamplesInstaller
|
||||
{
|
||||
private const string manualInstallURL = "https://docs.yarnspinner.dev/next/yarn-spinner-for-game-engines/unity/installation-and-setup#installing-the-samples";
|
||||
internal static void InstallSamples()
|
||||
{
|
||||
if (YarnPackageImporter.IsSamplesPackageInstalled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UnityEngine.Application.OpenURL(manualInstallURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23020a8d4675148c9ad05af13ab1202b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
606
Packages/dev.yarnspinner.unity/Editor/Utility/MiniJSON.cs
Normal file
606
Packages/dev.yarnspinner.unity/Editor/Utility/MiniJSON.cs
Normal file
@@ -0,0 +1,606 @@
|
||||
/*
|
||||
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2013 Calvin Rien
|
||||
*
|
||||
* Based on the JSON parser by Patrick van Bergen
|
||||
* http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
|
||||
*
|
||||
* Simplified it so that it doesn't throw exceptions
|
||||
* and can be used in Unity iPhone with maximum code stripping.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
// Nullability disabled due to third-party code
|
||||
#nullable disable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// This class encodes and decodes JSON strings.
|
||||
/// Spec. details, see http://www.json.org/
|
||||
///
|
||||
/// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary.
|
||||
/// All numbers are parsed to doubles.
|
||||
/// </summary>
|
||||
public static class Json
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the string json into a value
|
||||
/// </summary>
|
||||
/// <param name="json">A JSON string.</param>
|
||||
/// <returns>An List<object>, a Dictionary<string, object>, a double, an integer,a string, null, true, or false</returns>
|
||||
public static object Deserialize(string json)
|
||||
{
|
||||
// save the string for debug information
|
||||
if (json == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Parser.Parse(json);
|
||||
}
|
||||
|
||||
sealed class Parser : IDisposable
|
||||
{
|
||||
const string WORD_BREAK = "{}[],:\"";
|
||||
|
||||
public static bool IsWordBreak(char c)
|
||||
{
|
||||
return Char.IsWhiteSpace(c) || WORD_BREAK.IndexOf(c) != -1;
|
||||
}
|
||||
|
||||
enum TOKEN
|
||||
{
|
||||
NONE,
|
||||
CURLY_OPEN,
|
||||
CURLY_CLOSE,
|
||||
SQUARED_OPEN,
|
||||
SQUARED_CLOSE,
|
||||
COLON,
|
||||
COMMA,
|
||||
STRING,
|
||||
NUMBER,
|
||||
TRUE,
|
||||
FALSE,
|
||||
NULL
|
||||
};
|
||||
|
||||
StringReader json;
|
||||
|
||||
Parser(string jsonString)
|
||||
{
|
||||
json = new StringReader(jsonString);
|
||||
}
|
||||
|
||||
public static object Parse(string jsonString)
|
||||
{
|
||||
using (var instance = new Parser(jsonString))
|
||||
{
|
||||
return instance.ParseValue();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
json.Dispose();
|
||||
json = null;
|
||||
}
|
||||
|
||||
Dictionary<string, object> ParseObject()
|
||||
{
|
||||
Dictionary<string, object> table = new Dictionary<string, object>();
|
||||
|
||||
// ditch opening brace
|
||||
json.Read();
|
||||
|
||||
// {
|
||||
while (true)
|
||||
{
|
||||
switch (NextToken)
|
||||
{
|
||||
case TOKEN.NONE:
|
||||
return null;
|
||||
case TOKEN.COMMA:
|
||||
continue;
|
||||
case TOKEN.CURLY_CLOSE:
|
||||
return table;
|
||||
default:
|
||||
// name
|
||||
string name = ParseString();
|
||||
if (name == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// :
|
||||
if (NextToken != TOKEN.COLON)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// ditch the colon
|
||||
json.Read();
|
||||
|
||||
// value
|
||||
table[name] = ParseValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<object> ParseArray()
|
||||
{
|
||||
List<object> array = new List<object>();
|
||||
|
||||
// ditch opening bracket
|
||||
json.Read();
|
||||
|
||||
// [
|
||||
var parsing = true;
|
||||
while (parsing)
|
||||
{
|
||||
TOKEN nextToken = NextToken;
|
||||
|
||||
switch (nextToken)
|
||||
{
|
||||
case TOKEN.NONE:
|
||||
return null;
|
||||
case TOKEN.COMMA:
|
||||
continue;
|
||||
case TOKEN.SQUARED_CLOSE:
|
||||
parsing = false;
|
||||
break;
|
||||
default:
|
||||
object value = ParseByToken(nextToken);
|
||||
|
||||
array.Add(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
object ParseValue()
|
||||
{
|
||||
TOKEN nextToken = NextToken;
|
||||
return ParseByToken(nextToken);
|
||||
}
|
||||
|
||||
object ParseByToken(TOKEN token)
|
||||
{
|
||||
switch (token)
|
||||
{
|
||||
case TOKEN.STRING:
|
||||
return ParseString();
|
||||
case TOKEN.NUMBER:
|
||||
return ParseNumber();
|
||||
case TOKEN.CURLY_OPEN:
|
||||
return ParseObject();
|
||||
case TOKEN.SQUARED_OPEN:
|
||||
return ParseArray();
|
||||
case TOKEN.TRUE:
|
||||
return true;
|
||||
case TOKEN.FALSE:
|
||||
return false;
|
||||
case TOKEN.NULL:
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
string ParseString()
|
||||
{
|
||||
StringBuilder s = new StringBuilder();
|
||||
char c;
|
||||
|
||||
// ditch opening quote
|
||||
json.Read();
|
||||
|
||||
bool parsing = true;
|
||||
while (parsing)
|
||||
{
|
||||
|
||||
if (json.Peek() == -1)
|
||||
{
|
||||
parsing = false;
|
||||
break;
|
||||
}
|
||||
|
||||
c = NextChar;
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
parsing = false;
|
||||
break;
|
||||
case '\\':
|
||||
if (json.Peek() == -1)
|
||||
{
|
||||
parsing = false;
|
||||
break;
|
||||
}
|
||||
|
||||
c = NextChar;
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
s.Append(c);
|
||||
break;
|
||||
case 'b':
|
||||
s.Append('\b');
|
||||
break;
|
||||
case 'f':
|
||||
s.Append('\f');
|
||||
break;
|
||||
case 'n':
|
||||
s.Append('\n');
|
||||
break;
|
||||
case 'r':
|
||||
s.Append('\r');
|
||||
break;
|
||||
case 't':
|
||||
s.Append('\t');
|
||||
break;
|
||||
case 'u':
|
||||
var hex = new char[4];
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
hex[i] = NextChar;
|
||||
}
|
||||
|
||||
s.Append((char)Convert.ToInt32(new string(hex), 16));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
s.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return s.ToString();
|
||||
}
|
||||
|
||||
object ParseNumber()
|
||||
{
|
||||
string number = NextWord;
|
||||
|
||||
if (number.IndexOf('.') == -1)
|
||||
{
|
||||
long parsedInt;
|
||||
Int64.TryParse(number, out parsedInt);
|
||||
return parsedInt;
|
||||
}
|
||||
|
||||
double parsedDouble;
|
||||
Double.TryParse(number, out parsedDouble);
|
||||
return parsedDouble;
|
||||
}
|
||||
|
||||
void EatWhitespace()
|
||||
{
|
||||
while (Char.IsWhiteSpace(PeekChar))
|
||||
{
|
||||
json.Read();
|
||||
|
||||
if (json.Peek() == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char PeekChar
|
||||
{
|
||||
get
|
||||
{
|
||||
return Convert.ToChar(json.Peek());
|
||||
}
|
||||
}
|
||||
|
||||
char NextChar
|
||||
{
|
||||
get
|
||||
{
|
||||
return Convert.ToChar(json.Read());
|
||||
}
|
||||
}
|
||||
|
||||
string NextWord
|
||||
{
|
||||
get
|
||||
{
|
||||
StringBuilder word = new StringBuilder();
|
||||
|
||||
while (!IsWordBreak(PeekChar))
|
||||
{
|
||||
word.Append(NextChar);
|
||||
|
||||
if (json.Peek() == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return word.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
TOKEN NextToken
|
||||
{
|
||||
get
|
||||
{
|
||||
EatWhitespace();
|
||||
|
||||
if (json.Peek() == -1)
|
||||
{
|
||||
return TOKEN.NONE;
|
||||
}
|
||||
|
||||
switch (PeekChar)
|
||||
{
|
||||
case '{':
|
||||
return TOKEN.CURLY_OPEN;
|
||||
case '}':
|
||||
json.Read();
|
||||
return TOKEN.CURLY_CLOSE;
|
||||
case '[':
|
||||
return TOKEN.SQUARED_OPEN;
|
||||
case ']':
|
||||
json.Read();
|
||||
return TOKEN.SQUARED_CLOSE;
|
||||
case ',':
|
||||
json.Read();
|
||||
return TOKEN.COMMA;
|
||||
case '"':
|
||||
return TOKEN.STRING;
|
||||
case ':':
|
||||
return TOKEN.COLON;
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
case '-':
|
||||
return TOKEN.NUMBER;
|
||||
}
|
||||
|
||||
switch (NextWord)
|
||||
{
|
||||
case "false":
|
||||
return TOKEN.FALSE;
|
||||
case "true":
|
||||
return TOKEN.TRUE;
|
||||
case "null":
|
||||
return TOKEN.NULL;
|
||||
}
|
||||
|
||||
return TOKEN.NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string
|
||||
/// </summary>
|
||||
/// <param name="json">A Dictionary<string, object> / List<object></param>
|
||||
/// <returns>A JSON encoded string, or null if object 'json' is not serializable</returns>
|
||||
public static string Serialize(object obj)
|
||||
{
|
||||
return Serializer.Serialize(obj);
|
||||
}
|
||||
|
||||
sealed class Serializer
|
||||
{
|
||||
StringBuilder builder;
|
||||
|
||||
Serializer()
|
||||
{
|
||||
builder = new StringBuilder();
|
||||
}
|
||||
|
||||
public static string Serialize(object obj)
|
||||
{
|
||||
var instance = new Serializer();
|
||||
|
||||
instance.SerializeValue(obj);
|
||||
|
||||
return instance.builder.ToString();
|
||||
}
|
||||
|
||||
void SerializeValue(object value)
|
||||
{
|
||||
IList asList;
|
||||
IDictionary asDict;
|
||||
string asStr;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
builder.Append("null");
|
||||
}
|
||||
else if ((asStr = value as string) != null)
|
||||
{
|
||||
SerializeString(asStr);
|
||||
}
|
||||
else if (value is bool)
|
||||
{
|
||||
builder.Append((bool)value ? "true" : "false");
|
||||
}
|
||||
else if ((asList = value as IList) != null)
|
||||
{
|
||||
SerializeArray(asList);
|
||||
}
|
||||
else if ((asDict = value as IDictionary) != null)
|
||||
{
|
||||
SerializeObject(asDict);
|
||||
}
|
||||
else if (value is char)
|
||||
{
|
||||
SerializeString(new string((char)value, 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
SerializeOther(value);
|
||||
}
|
||||
}
|
||||
|
||||
void SerializeObject(IDictionary obj)
|
||||
{
|
||||
bool first = true;
|
||||
|
||||
builder.Append('{');
|
||||
|
||||
foreach (object e in obj.Keys)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
SerializeString(e.ToString());
|
||||
builder.Append(':');
|
||||
|
||||
SerializeValue(obj[e]);
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
builder.Append('}');
|
||||
}
|
||||
|
||||
void SerializeArray(IList anArray)
|
||||
{
|
||||
builder.Append('[');
|
||||
|
||||
bool first = true;
|
||||
|
||||
foreach (object obj in anArray)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
SerializeValue(obj);
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
builder.Append(']');
|
||||
}
|
||||
|
||||
void SerializeString(string str)
|
||||
{
|
||||
builder.Append('\"');
|
||||
|
||||
char[] charArray = str.ToCharArray();
|
||||
foreach (var c in charArray)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
builder.Append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
builder.Append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
builder.Append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
builder.Append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
builder.Append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.Append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
builder.Append("\\t");
|
||||
break;
|
||||
default:
|
||||
int codepoint = Convert.ToInt32(c);
|
||||
if ((codepoint >= 32) && (codepoint <= 126))
|
||||
{
|
||||
builder.Append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("\\u");
|
||||
builder.Append(codepoint.ToString("x4"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append('\"');
|
||||
}
|
||||
|
||||
void SerializeOther(object value)
|
||||
{
|
||||
// NOTE: decimals lose precision during serialization.
|
||||
// They always have, I'm just letting you know.
|
||||
// Previously floats and doubles lost precision too.
|
||||
if (value is float)
|
||||
{
|
||||
builder.Append(((float)value).ToString("R"));
|
||||
}
|
||||
else if (value is int
|
||||
|| value is uint
|
||||
|| value is long
|
||||
|| value is sbyte
|
||||
|| value is byte
|
||||
|| value is short
|
||||
|| value is ushort
|
||||
|| value is ulong)
|
||||
{
|
||||
builder.Append(value);
|
||||
}
|
||||
else if (value is double
|
||||
|| value is decimal)
|
||||
{
|
||||
builder.Append(Convert.ToDouble(value).ToString("R"));
|
||||
}
|
||||
else
|
||||
{
|
||||
SerializeString(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b4adfe2cc33c4714a7fad372d96d61e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,108 @@
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
internal static class UPMSamplesInstaller
|
||||
{
|
||||
private const string samplesPackageURL = "https://github.com/YarnSpinnerTool/YarnSpinner-Unity-Samples.git#current";
|
||||
private static AddRequest? installationRequest;
|
||||
|
||||
private static void MonitorInstallation()
|
||||
{
|
||||
if (installationRequest == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!installationRequest.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (installationRequest.Status == StatusCode.Failure)
|
||||
{
|
||||
// it failed, log the error but don't clean up the installation
|
||||
// request as we need the error it has for determining the
|
||||
// status
|
||||
UnityEngine.Debug.LogError(installationRequest.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
// we succeeded, so we wipe out the request
|
||||
installationRequest = null;
|
||||
}
|
||||
// we remove ourselves from the update loop
|
||||
UnityEditor.EditorApplication.update -= MonitorInstallation;
|
||||
}
|
||||
|
||||
internal static void InstallSamples()
|
||||
{
|
||||
switch (Status)
|
||||
{
|
||||
case YarnPackageImporter.SamplesPackageStatus.Installed:
|
||||
{
|
||||
// we already have it, ignoring this
|
||||
break;
|
||||
}
|
||||
case YarnPackageImporter.SamplesPackageStatus.NotInstalled:
|
||||
{
|
||||
// it's not installed so we need to request it
|
||||
installationRequest = Client.Add(samplesPackageURL);
|
||||
UnityEditor.EditorApplication.update += MonitorInstallation;
|
||||
break;
|
||||
}
|
||||
case YarnPackageImporter.SamplesPackageStatus.Installing:
|
||||
{
|
||||
// its in progress so just wait, jeez
|
||||
break;
|
||||
}
|
||||
case YarnPackageImporter.SamplesPackageStatus.FailedToInstall:
|
||||
{
|
||||
// it failed but that's fine, we can just go again!
|
||||
installationRequest = Client.Add(samplesPackageURL);
|
||||
UnityEditor.EditorApplication.update += MonitorInstallation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static YarnPackageImporter.SamplesPackageStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
// ok so first things first if the package is installed we can
|
||||
// say that
|
||||
if (YarnPackageImporter.IsSamplesPackageInstalled)
|
||||
{
|
||||
return YarnPackageImporter.SamplesPackageStatus.Installed;
|
||||
}
|
||||
|
||||
// we aren't installed but we could be one of:
|
||||
// - installing in progress
|
||||
// - failed while attempting an install
|
||||
// - not even attempted to install it
|
||||
if (installationRequest != null)
|
||||
{
|
||||
// we might be in the process of installing
|
||||
if (installationRequest.Status == StatusCode.InProgress)
|
||||
{
|
||||
return YarnPackageImporter.SamplesPackageStatus.Installing;
|
||||
}
|
||||
|
||||
// at this point we must have had a failure, so we report
|
||||
// that
|
||||
if (installationRequest.Status == StatusCode.Failure)
|
||||
{
|
||||
return YarnPackageImporter.SamplesPackageStatus.FailedToInstall;
|
||||
}
|
||||
}
|
||||
|
||||
// at this point we simply aren't installed
|
||||
return YarnPackageImporter.SamplesPackageStatus.NotInstalled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc1506e515b6046b1939407847cb96e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
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)!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d9a462ef0480497085e70c75081cf1b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
public static partial class YarnPackageImporter
|
||||
{
|
||||
// what install approach do we follow?
|
||||
public const InstallApproach InstallationApproach = InstallApproach.Manual;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e75395ea6a314c868fb875f5f16fab0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.UI;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
public class YarnPackageImporterException : Exception
|
||||
{
|
||||
public YarnPackageImporterException() { }
|
||||
|
||||
public YarnPackageImporterException(string message)
|
||||
: base(message) { }
|
||||
|
||||
public YarnPackageImporterException(string message, Exception inner)
|
||||
: base(message, inner) { }
|
||||
}
|
||||
|
||||
public static partial class YarnPackageImporter
|
||||
{
|
||||
public enum SamplesPackageStatus
|
||||
{
|
||||
Installed, NotInstalled, Installing, FailedToInstall
|
||||
}
|
||||
|
||||
private const string yarnSpinnerPackageName = "dev.yarnspinner.unity";
|
||||
private const string samplesPackageName = "dev.yarnspinner.unity.samples";
|
||||
|
||||
public enum InstallApproach
|
||||
{
|
||||
Itch, AssetStore, Manual
|
||||
}
|
||||
|
||||
// What is the status of the samples package?
|
||||
public static SamplesPackageStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
// if we have the samples we don't really care about HOW, so
|
||||
// just return that
|
||||
if (IsSamplesPackageInstalled)
|
||||
{
|
||||
return SamplesPackageStatus.Installed;
|
||||
}
|
||||
|
||||
// each approach has their own approach for this, at this stage
|
||||
// only UPM can really do anything. so in that case we bounce
|
||||
// out to it, and for all others they are not installed later on
|
||||
// this will ideally change
|
||||
#pragma warning disable 162
|
||||
switch (InstallationApproach)
|
||||
{
|
||||
case InstallApproach.Manual:
|
||||
{
|
||||
return UPMSamplesInstaller.Status;
|
||||
}
|
||||
}
|
||||
return SamplesPackageStatus.NotInstalled;
|
||||
#pragma warning restore
|
||||
}
|
||||
}
|
||||
|
||||
// install the samples package if not installed
|
||||
[UnityEditor.MenuItem("Window/Yarn Spinner/Install Samples Package", false)]
|
||||
internal static void InstallSamples()
|
||||
{
|
||||
#pragma warning disable 162
|
||||
switch (InstallationApproach)
|
||||
{
|
||||
// there are two variants here
|
||||
case InstallApproach.Manual:
|
||||
{
|
||||
if (IsYarnSpinnerPackageInstalled)
|
||||
{
|
||||
// if the yarn spinner package is installed as a
|
||||
// package we want to also install the samples this
|
||||
// way
|
||||
UPMSamplesInstaller.InstallSamples();
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise it's a fully manually vendored version
|
||||
// of YS
|
||||
ManualSamplesInstaller.InstallSamples();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InstallApproach.Itch:
|
||||
{
|
||||
ItchSamplesInstaller.InstallSamples();
|
||||
break;
|
||||
}
|
||||
case InstallApproach.AssetStore:
|
||||
{
|
||||
AssetStoreSamplesInstaller.InstallSamples();
|
||||
break;
|
||||
}
|
||||
}
|
||||
#pragma warning restore
|
||||
}
|
||||
|
||||
// open the samples up if they are installed
|
||||
private static void ShowSamples()
|
||||
{
|
||||
if (IsSamplesPackageInstalled)
|
||||
{
|
||||
Window.Open(samplesPackageName);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_2022_3_33_OR_NEWER
|
||||
static PackageInfo? GetInstalledPackageInfo(string packageName)
|
||||
{
|
||||
return PackageInfo.FindForPackageName(packageName);
|
||||
}
|
||||
#else
|
||||
// prior to 2022.3.33f1 they didn't have a good way to get a specific selected package
|
||||
// so instead what we do is run through every installed package and see if it has the same name
|
||||
// if it does we return that
|
||||
// otherwise we return null.
|
||||
// In my testing this hasn't caused any issues but I am sure there are gaps I have missed
|
||||
// which I think is an acceptable tradeoff considering the age of <.33
|
||||
// and the failure state is that instead of opening the samples we open the docs
|
||||
// which feels ok to me as a fallback.
|
||||
static PackageInfo? GetInstalledPackageInfo(string packageName)
|
||||
{
|
||||
var allPackages = PackageInfo.GetAllRegisteredPackages();
|
||||
foreach (var package in allPackages)
|
||||
{
|
||||
if (package.name.ToLower() == packageName.ToLower())
|
||||
{
|
||||
return package;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
|
||||
static IEnumerable<Sample> GetSamplesForInstalledPackage(PackageInfo package)
|
||||
{
|
||||
return Sample.FindByPackage(package.name, package.version);
|
||||
}
|
||||
|
||||
private static void InstallPackageSamples(PackageInfo package)
|
||||
{
|
||||
IEnumerable<Sample> samples = GetSamplesForInstalledPackage(package);
|
||||
InstallPackageSamples(samples);
|
||||
}
|
||||
|
||||
private static void InstallPackageSamples(IEnumerable<Sample> samples)
|
||||
{
|
||||
List<Sample> failedSamples = new();
|
||||
foreach (Sample sample in samples)
|
||||
{
|
||||
if (!sample.isImported && !sample.Import())
|
||||
{
|
||||
failedSamples.Add(sample);
|
||||
}
|
||||
}
|
||||
if (failedSamples.Any())
|
||||
{
|
||||
throw new YarnPackageImporterException($"{failedSamples.Count} samples failed to install.");
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Yarn Spinner Package
|
||||
|
||||
public static PackageInfo? YarnSpinnerPackageInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetInstalledPackageInfo(yarnSpinnerPackageName);
|
||||
}
|
||||
}
|
||||
|
||||
public static string? YarnSpinnerPackageVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
PackageInfo? yarnspinnerPackage = YarnSpinnerPackageInfo;
|
||||
return yarnspinnerPackage?.version;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsYarnSpinnerPackageInstalled
|
||||
{
|
||||
get
|
||||
{
|
||||
return YarnSpinnerPackageInfo != null;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Samples Package
|
||||
|
||||
public static PackageInfo? SamplesPackageInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetInstalledPackageInfo(samplesPackageName);
|
||||
}
|
||||
}
|
||||
|
||||
public static string? SamplesPackageVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
PackageInfo? samplesPackage = SamplesPackageInfo;
|
||||
return samplesPackage?.version;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSamplesPackageInstalled
|
||||
{
|
||||
get
|
||||
{
|
||||
return SamplesPackageInfo != null;
|
||||
}
|
||||
}
|
||||
|
||||
[UnityEditor.MenuItem("Window/Yarn Spinner/Install Samples Package", true)]
|
||||
public static bool InstallSamplesPackageValidation()
|
||||
{
|
||||
return !IsSamplesPackageInstalled;
|
||||
}
|
||||
|
||||
public static IEnumerable<Sample> GetSamplesPackageSamples()
|
||||
{
|
||||
if (SamplesPackageInfo != null)
|
||||
{
|
||||
return GetSamplesForInstalledPackage(SamplesPackageInfo);
|
||||
}
|
||||
return new List<Sample>();
|
||||
}
|
||||
|
||||
public static void OpenSamplesUI()
|
||||
{
|
||||
if (!IsSamplesPackageInstalled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ShowSamples();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec4788e44f8ba48539f44859c5fe921d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,732 @@
|
||||
/*
|
||||
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 UnityEditor.Callbacks;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
#if USE_ADDRESSABLES
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
#endif
|
||||
|
||||
#if !UNITY_6000_4_OR_NEWER
|
||||
// EntityId was introduced in Unity 6.4 to replace integer-based instance IDs.
|
||||
// We'll use a type alias so that versions earlier than that can keep working
|
||||
// with the older APIs.
|
||||
using EntityId = System.Int32;
|
||||
#endif
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Yarn.Unity.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains methods for performing high-level operations on Yarn projects,
|
||||
/// and their associated localization files.
|
||||
/// </summary>
|
||||
internal static class YarnProjectUtility
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new .yarnproject asset in the same directory as the Yarn
|
||||
/// script represented by <paramref name="initialSourceAsset"/>, and
|
||||
/// configures the script's importer to use the new Yarn Project.
|
||||
/// </summary>
|
||||
/// <param name="initialSourceAsset">An importer for an existing Yarn
|
||||
/// script.</param>
|
||||
/// <returns>The path to the created asset, relative to the Unity
|
||||
/// project root.</returns>
|
||||
internal static string CreateYarnProject(YarnImporter initialSourceAsset)
|
||||
{
|
||||
|
||||
// Figure out where on disk this asset is
|
||||
var path = initialSourceAsset.assetPath;
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
|
||||
// Figure out a new, unique path for the localization we're creating
|
||||
var databaseFileName = $"Project.yarnproject";
|
||||
|
||||
var destinationPath = Path.Combine(directory, databaseFileName);
|
||||
destinationPath = AssetDatabase.GenerateUniqueAssetPath(destinationPath);
|
||||
|
||||
// Create the program
|
||||
var newProject = YarnProjectUtility.CreateDefaultYarnProject();
|
||||
|
||||
newProject.SaveToFile(destinationPath);
|
||||
|
||||
AssetDatabase.ImportAsset(destinationPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return destinationPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Unity tweaked default Yarn Project.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is just a default Yarn Project with the exclusion file pattern
|
||||
/// set up to ignore ~ folders.
|
||||
/// </remarks>
|
||||
/// <returns>A Unity default Yarn Project</returns>
|
||||
internal static Yarn.Compiler.Project CreateDefaultYarnProject()
|
||||
{
|
||||
// Create the program
|
||||
var newProject = new Yarn.Compiler.Project();
|
||||
|
||||
// Follow Unity's behaviour - exclude any content in a folder whose
|
||||
// name ends with a tilde
|
||||
// and also ignoring anything that is inside a sample folder
|
||||
newProject.ExcludeFilePatterns = new[] { "**/*~/*", "./Samples/Yarn Spinner*/*" };
|
||||
|
||||
return newProject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates every localization .CSV file associated with this
|
||||
/// .yarnproject file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method updates each localization file by performing the
|
||||
/// following operations:
|
||||
/// <list type="bullet">
|
||||
/// <item>Inserts new entries if they're present in the base
|
||||
/// localization and not in the translated localization</item>
|
||||
///
|
||||
/// <item>Removes entries if they're present in the translated
|
||||
/// localization and not in the base localization</item>
|
||||
///
|
||||
/// <item>Detects if a line in the base localization has changed its
|
||||
/// Lock value from when the translated localization was created, and
|
||||
/// update its Comment</item></list>
|
||||
/// </remarks>
|
||||
/// <param name="yarnProjectImporter">An importer for an existing Yarn
|
||||
/// script.</param>
|
||||
/// <returns>The path to the created asset, relative to the Unity
|
||||
/// project root.</returns>
|
||||
internal static void UpdateLocalizationCSVs(YarnProjectImporter yarnProjectImporter)
|
||||
{
|
||||
if (yarnProjectImporter.CanGenerateStringsTable == false)
|
||||
{
|
||||
Debug.LogError($"Can't update localization CSVs for Yarn Project \"{yarnProjectImporter.name}\" because not every line has a tag.");
|
||||
return;
|
||||
}
|
||||
|
||||
var importData = yarnProjectImporter.ImportData;
|
||||
|
||||
if (importData == null)
|
||||
{
|
||||
Debug.LogError($"Can't update localization CSVs for Yarn Project \"{yarnProjectImporter.name}\" because it failed to compile.");
|
||||
return;
|
||||
}
|
||||
|
||||
var job = yarnProjectImporter.GetCompilationJob();
|
||||
job.CompilationType = Compiler.CompilationJob.Type.StringsOnly;
|
||||
var result = Compiler.Compiler.Compile(job);
|
||||
|
||||
var baseLocalizationStrings = yarnProjectImporter.GenerateStringsTable(result);
|
||||
|
||||
if (baseLocalizationStrings == null)
|
||||
{
|
||||
Debug.LogError($"Can't update localization CSVs for Yarn Project \"{yarnProjectImporter.name}\" because it failed to compile.");
|
||||
return;
|
||||
}
|
||||
|
||||
var localizations = importData.localizations;
|
||||
|
||||
var modifiedFiles = new List<TextAsset>();
|
||||
|
||||
try
|
||||
{
|
||||
AssetDatabase.StartAssetEditing();
|
||||
|
||||
foreach (var loc in localizations)
|
||||
{
|
||||
if (loc.languageID == importData.baseLanguageName)
|
||||
{
|
||||
// This is the base language - no strings file to
|
||||
// update.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loc.stringsFile == null)
|
||||
{
|
||||
Debug.LogWarning($"Can't update localization for {loc.languageID} because it doesn't have a strings file.", yarnProjectImporter);
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileWasChanged = UpdateLocalizationFile(baseLocalizationStrings, loc.languageID, loc.stringsFile);
|
||||
|
||||
if (fileWasChanged)
|
||||
{
|
||||
modifiedFiles.Add(loc.stringsFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiedFiles.Count > 0)
|
||||
{
|
||||
Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles.Select(f => AssetDatabase.GetAssetPath(f)))}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"No files needed updating.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AssetDatabase.StopAssetEditing();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void UpdateAssetAddresses(YarnProjectImporter importer)
|
||||
{
|
||||
#if USE_ADDRESSABLES
|
||||
var job = importer.GetCompilationJob();
|
||||
job.CompilationType = Compiler.CompilationJob.Type.StringsOnly;
|
||||
var result = Compiler.Compiler.Compile(job);
|
||||
|
||||
var lineIDs = importer.GenerateStringsTable(result).Select(s => s.ID);
|
||||
|
||||
if (importer.ImportData == null)
|
||||
{
|
||||
throw new System.InvalidOperationException($"Can't update asset addresses: importer has no {nameof(importer.ImportData)}");
|
||||
}
|
||||
|
||||
// Get a map of language IDs to (lineID, asset path) pairs
|
||||
var languageToAssets = importer
|
||||
// Get the languages-to-source-assets map
|
||||
.ImportData.localizations
|
||||
// Get the asset folder for them
|
||||
.Select(l => new { l.languageID, l.assetsFolder })
|
||||
// Only consider those that have an asset folder
|
||||
.Where(f => f.assetsFolder != null)
|
||||
// Get the path for the asset folder
|
||||
.Select(f => new { f.languageID, path = AssetDatabase.GetAssetPath(f.assetsFolder) })
|
||||
// Use that to get the assets inside these folders
|
||||
.Select(f => new { f.languageID, assetPaths = FindAssetPathsForLineIDs(lineIDs, f.path, typeof(UnityEngine.Object)) });
|
||||
|
||||
var addressableAssetSettings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
|
||||
foreach (var languageToAsset in languageToAssets)
|
||||
{
|
||||
var assets = languageToAsset.assetPaths
|
||||
.Select(pair => new { LineID = pair.Key, GUID = AssetDatabase.AssetPathToGUID(pair.Value) });
|
||||
|
||||
foreach (var asset in assets)
|
||||
{
|
||||
// Find the existing entry for this asset, if it has one.
|
||||
AddressableAssetEntry entry = addressableAssetSettings.FindAssetEntry(asset.GUID);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
// This asset didn't have an entry. Create one in the
|
||||
// default group.
|
||||
entry = addressableAssetSettings.CreateOrMoveEntry(asset.GUID, addressableAssetSettings.DefaultGroup);
|
||||
}
|
||||
|
||||
// Update the entry's address.
|
||||
entry.SetAddress(Localization.GetAddressForLine(asset.LineID, languageToAsset.languageID));
|
||||
}
|
||||
}
|
||||
#else
|
||||
throw new System.NotSupportedException($"A method that requires the Addressable Assets package was called, but USE_ADDRESSABLES was not defined. Please either install Addressable Assets, or if you have already, add it to this project's compiler definitions.");
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static Dictionary<string, string> FindAssetPathsForLineIDs(IEnumerable<string> lineIDs, string assetsFolderPath, System.Type assetType)
|
||||
{
|
||||
// Find _all_ files in this director that are not .meta files and
|
||||
// whose main asset is equal to (or derived from) assetType
|
||||
var allFiles = Directory.EnumerateFiles(assetsFolderPath, "*", SearchOption.AllDirectories)
|
||||
.Where(path => path.EndsWith(".meta") == false)
|
||||
.Where(path => assetType.IsAssignableFrom(AssetDatabase.GetMainAssetTypeAtPath(path)));
|
||||
|
||||
// Match files with those whose filenames contain a line ID
|
||||
// If a direct file match is found prefer that
|
||||
Dictionary<string, string> assets = new();
|
||||
foreach (var lineID in lineIDs)
|
||||
{
|
||||
var lineIDWithoutPrefix = lineID.Replace("line:", "").ToLowerInvariant();
|
||||
var candidates = new List<string>();
|
||||
foreach (var asset in allFiles)
|
||||
{
|
||||
var file = Path.GetFileNameWithoutExtension(asset).ToLowerInvariant();
|
||||
|
||||
if (file == lineIDWithoutPrefix)
|
||||
{
|
||||
assets[lineID] = asset;
|
||||
break;
|
||||
}
|
||||
|
||||
if (file.Contains(lineIDWithoutPrefix))
|
||||
{
|
||||
candidates.Add(asset);
|
||||
}
|
||||
}
|
||||
|
||||
var count = candidates.Count();
|
||||
if (count > 0)
|
||||
{
|
||||
assets[lineID] = candidates.FirstOrDefault();
|
||||
if (count > 1)
|
||||
{
|
||||
Debug.LogWarning($"Discovered {count} candidates for {lineID}. Selecting one.\nCandidates:\n" + string.Join("\n", candidates));
|
||||
}
|
||||
}
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the TextAsset referred to by <paramref
|
||||
/// name="destinationLocalizationAsset"/>, and updates it if necessary.
|
||||
/// </summary>
|
||||
/// <param name="baseLocalizationStrings">A collection of <see
|
||||
/// cref="StringTableEntry"/></param>
|
||||
/// <param name="language">The language that <paramref
|
||||
/// name="destinationLocalizationAsset"/> provides strings
|
||||
/// for.false</param>
|
||||
/// <param name="destinationLocalizationAsset">A TextAsset containing
|
||||
/// localized strings in CSV format.</param>
|
||||
/// <returns>Whether <paramref name="destinationLocalizationAsset"/> was
|
||||
/// modified.</returns>
|
||||
private static bool UpdateLocalizationFile(IEnumerable<StringTableEntry> baseLocalizationStrings, string language, TextAsset destinationLocalizationAsset)
|
||||
{
|
||||
var translatedStrings = StringTableEntry.ParseFromCSV(destinationLocalizationAsset.text);
|
||||
|
||||
// Convert both enumerables to dictionaries, for easier lookup
|
||||
var baseDictionary = baseLocalizationStrings.ToDictionary(entry => entry.ID);
|
||||
var translatedDictionary = translatedStrings.ToDictionary(entry => entry.ID);
|
||||
|
||||
// The list of line IDs present in each localisation
|
||||
var baseIDs = baseLocalizationStrings.Select(entry => entry.ID);
|
||||
var translatedIDs = translatedStrings.Select(entry => entry.ID);
|
||||
|
||||
// The list of line IDs that are ONLY present in each localisation
|
||||
var onlyInBaseIDs = baseIDs.Except(translatedIDs);
|
||||
var onlyInTranslatedIDs = translatedIDs.Except(baseIDs);
|
||||
|
||||
// Tracks if the translated localisation needed modifications
|
||||
// (either new lines added, old lines removed, or changed lines
|
||||
// flagged)
|
||||
var modificationsNeeded = false;
|
||||
|
||||
// Remove every entry whose ID is only present in the translated
|
||||
// set. This entry has been removed from the base localization.
|
||||
foreach (var id in onlyInTranslatedIDs.ToList())
|
||||
{
|
||||
translatedDictionary.Remove(id);
|
||||
modificationsNeeded = true;
|
||||
}
|
||||
|
||||
// Conversely, for every entry that is only present in the base
|
||||
// localisation, we need to create a new entry for it.
|
||||
foreach (var id in onlyInBaseIDs)
|
||||
{
|
||||
StringTableEntry baseEntry = baseDictionary[id];
|
||||
var newEntry = new StringTableEntry(baseEntry)
|
||||
{
|
||||
// Empty this text, so that it's apparent that a translated
|
||||
// version needs to be provided.
|
||||
Text = string.Empty,
|
||||
Language = language,
|
||||
};
|
||||
translatedDictionary.Add(id, newEntry);
|
||||
modificationsNeeded = true;
|
||||
}
|
||||
|
||||
// Finally, we need to check for any entries in the translated
|
||||
// localisation that:
|
||||
// 1. have the same line ID as one in the base, but
|
||||
// 2. have a different Lock (the hash of the text), which indicates
|
||||
// that the base text has changed.
|
||||
|
||||
// First, get the list of IDs that are in both base and translated,
|
||||
// and then filter this list to any where the lock values differ
|
||||
var outOfDateLockIDs = baseDictionary.Keys
|
||||
.Intersect(translatedDictionary.Keys)
|
||||
.Where(id => baseDictionary[id].Lock != translatedDictionary[id].Lock);
|
||||
|
||||
// Now loop over all of these, and update our translated dictionary
|
||||
// to include a note that it needs attention
|
||||
foreach (var id in outOfDateLockIDs)
|
||||
{
|
||||
// Get the translated entry as it currently exists
|
||||
var entry = translatedDictionary[id];
|
||||
|
||||
// Include a note that this entry is out of date
|
||||
entry.Text = $"(NEEDS UPDATE) {entry.Text}";
|
||||
|
||||
// Update the lock to match the new one
|
||||
entry.Lock = baseDictionary[id].Lock;
|
||||
|
||||
// Put this modified entry back in the table
|
||||
translatedDictionary[id] = entry;
|
||||
|
||||
modificationsNeeded = true;
|
||||
}
|
||||
|
||||
// We're all done!
|
||||
|
||||
if (modificationsNeeded == false)
|
||||
{
|
||||
// No changes needed to be done to the translated string table
|
||||
// entries. Stop here.
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to produce a replacement CSV file for the translated
|
||||
// entries.
|
||||
|
||||
var outputStringEntries = translatedDictionary.Values
|
||||
.OrderBy(entry => entry.File)
|
||||
.ThenBy(entry => int.Parse(entry.LineNumber));
|
||||
|
||||
var outputCSV = StringTableEntry.CreateCSV(outputStringEntries);
|
||||
|
||||
// Write out the replacement text to this existing file, replacing
|
||||
// its existing contents
|
||||
var outputFile = AssetDatabase.GetAssetPath(destinationLocalizationAsset);
|
||||
File.WriteAllText(outputFile, outputCSV, System.Text.Encoding.UTF8);
|
||||
|
||||
// Tell the asset database that the file needs to be reimported
|
||||
AssetDatabase.ImportAsset(outputFile);
|
||||
|
||||
// Signal that the file was changed
|
||||
return true;
|
||||
}
|
||||
|
||||
private static (List<string> AllExistingTags, List<string> ProjectImplicitTags) ExtantLineTags(YarnProjectImporter importer)
|
||||
{
|
||||
// First, gather all existing line tags across ALL yarn projects, so
|
||||
// that we don't accidentally overwrite an existing one. Do this by
|
||||
// finding all yarn projects, and get the string tags inside them.
|
||||
// By doing it in this way we get the same implicit tags from the
|
||||
// project as the importer would normally do, letting us then do a
|
||||
// direct comparision for them.
|
||||
var allYarnProjects =
|
||||
// get all yarn projects across the entire project
|
||||
AssetDatabase.FindAssets($"t:{nameof(YarnProject)}")
|
||||
// Get the path for each asset's GUID
|
||||
.Select(guid => AssetDatabase.GUIDToAssetPath(guid))
|
||||
// Get the importer for each asset at this path
|
||||
.Select(path => AssetImporter.GetAtPath(path))
|
||||
// Ensure it's a YarnProjectImporter
|
||||
.OfType<YarnProjectImporter>()
|
||||
// Ensure that its import data is present
|
||||
.Where(i => i.ImportData != null)
|
||||
// get the project out, and also flag if it is the project for
|
||||
// THIS importer
|
||||
.Select(i => (Project: i.GetProject()!, IsThisImporter: i == importer))
|
||||
// remove any nulls just in case any are found
|
||||
.Where(p => p.Project != null);
|
||||
|
||||
#if YARNSPINNER_DEBUG
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
#endif
|
||||
|
||||
var allExistingTags = new List<string>();
|
||||
var projectImplicitTags = new List<string>();
|
||||
|
||||
// Compile all of these, and get whatever existing string tags they
|
||||
// had. Do each in isolation so that we can continue even if a
|
||||
// project contains a parse error.
|
||||
foreach (var (Project, IsThisImporter) in allYarnProjects)
|
||||
{
|
||||
var project = Project;
|
||||
var compilationJob = Yarn.Compiler.CompilationJob.CreateFromFiles(project.SourceFiles);
|
||||
compilationJob.CompilationType = Yarn.Compiler.CompilationJob.Type.StringsOnly;
|
||||
|
||||
var result = Yarn.Compiler.Compiler.Compile(compilationJob);
|
||||
bool containsErrors = result.Diagnostics.Any(d => d.Severity == Compiler.Diagnostic.DiagnosticSeverity.Error);
|
||||
if (containsErrors)
|
||||
{
|
||||
Debug.LogWarning($"{project} has errors so cannot be scanned for tagging.");
|
||||
continue;
|
||||
}
|
||||
allExistingTags.AddRange(result.StringTable.Where(i => i.Value.isImplicitTag == false).Select(i => i.Key));
|
||||
|
||||
// we add the implicit lines IDs only for this project
|
||||
if (IsThisImporter)
|
||||
{
|
||||
projectImplicitTags.AddRange(result.StringTable.Where(i => i.Value.isImplicitTag == true).Select(i => i.Key));
|
||||
}
|
||||
}
|
||||
|
||||
#if YARNSPINNER_DEBUG
|
||||
stopwatch.Stop();
|
||||
Debug.Log($"Checked {allYarnProjects.Count()} yarn files for line tags in {stopwatch.ElapsedMilliseconds}ms");
|
||||
#endif
|
||||
return (allExistingTags, projectImplicitTags);
|
||||
}
|
||||
|
||||
public static void AddLineTagsToFilesInYarnProject(YarnProjectImporter importer)
|
||||
{
|
||||
var (AllExistingTags, ProjectImplicitTags) = YarnProjectUtility.ExtantLineTags(importer);
|
||||
|
||||
#if USE_UNITY_LOCALIZATION
|
||||
// if we are using Unity localisation we need to first remove the
|
||||
// implicit tags for this project from the strings table
|
||||
if (importer.UseUnityLocalisationSystem && importer.UnityLocalisationStringTableCollection != null)
|
||||
{
|
||||
foreach (var implicitTag in ProjectImplicitTags)
|
||||
{
|
||||
importer.UnityLocalisationStringTableCollection.RemoveEntry(implicitTag);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (importer.ImportData == null)
|
||||
{
|
||||
Debug.LogError($"Can't add line tags to {importer.assetPath}, because it failed to compile.");
|
||||
return;
|
||||
}
|
||||
|
||||
var modifiedFiles = new List<string>();
|
||||
try
|
||||
{
|
||||
AssetDatabase.StartAssetEditing();
|
||||
|
||||
foreach (var script in importer.ImportData.yarnFiles)
|
||||
{
|
||||
var assetPath = AssetDatabase.GetAssetPath(script);
|
||||
var contents = File.ReadAllText(assetPath);
|
||||
|
||||
// Produce a version of this file that contains line tags
|
||||
// added where they're needed.
|
||||
var tagged = Yarn.Compiler.Utility.TagLines(contents, AllExistingTags ?? new List<string>());
|
||||
var taggedVersion = tagged.Item1;
|
||||
|
||||
// if the file has an error it returns null we want to bail
|
||||
// out then otherwise we'd wipe the yarn file
|
||||
if (taggedVersion == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this produced a modified version of the file, write it
|
||||
// out and re-import it.
|
||||
if (contents != taggedVersion)
|
||||
{
|
||||
modifiedFiles.Add(Path.GetFileNameWithoutExtension(assetPath));
|
||||
|
||||
File.WriteAllText(assetPath, taggedVersion, System.Text.Encoding.UTF8);
|
||||
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.Default);
|
||||
|
||||
AllExistingTags = tagged.Item2 as List<string>;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"Encountered an error when updating scripts: {e}");
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AssetDatabase.StopAssetEditing();
|
||||
}
|
||||
|
||||
// Report on the work we did.
|
||||
if (modifiedFiles.Count > 0)
|
||||
{
|
||||
Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("No files needed updating.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a .csv file to disk at the path indicated by <paramref
|
||||
/// name="destination"/>, containing all of the lines found in the
|
||||
/// scripts referred to by <paramref name="yarnProjectImporter"/>.
|
||||
/// </summary>
|
||||
/// <param name="yarnProjectImporter">The YarnProjectImporter to extract
|
||||
/// strings from.</param>
|
||||
/// <param name="destination">The path to write the file to.</param>
|
||||
/// <returns><see langword="true"/> if the file was written
|
||||
/// successfully, <see langword="false"/> otherwise.</returns>
|
||||
/// <exception cref="CsvHelper.CsvHelperException">Thrown when an error
|
||||
/// is encountered when generating the CSV data.</exception>
|
||||
/// <exception cref="IOException">Thrown when an error is encountered
|
||||
/// when writing the data to disk.</exception>
|
||||
internal static bool WriteStringsFile(string destination, YarnProjectImporter yarnProjectImporter)
|
||||
{
|
||||
// Perform a strings-only compilation to get a full strings table,
|
||||
// and generate the CSV.
|
||||
var job = yarnProjectImporter.GetCompilationJob();
|
||||
job.CompilationType = Compiler.CompilationJob.Type.StringsOnly;
|
||||
var result = Compiler.Compiler.Compile(job);
|
||||
|
||||
if (result.ContainsErrors)
|
||||
{
|
||||
// The project contains errors. Bail out.
|
||||
return false;
|
||||
}
|
||||
|
||||
var stringTable = yarnProjectImporter.GenerateStringsTable(result);
|
||||
|
||||
// If there was an error, bail out here
|
||||
if (stringTable == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert the string tables to CSV...
|
||||
var outputCSV = StringTableEntry.CreateCSV(stringTable);
|
||||
|
||||
// ...and write it to disk.
|
||||
File.WriteAllText(destination, outputCSV);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a .csv file to disk at the path indicated by <paramref
|
||||
/// name="destination"/>, containing all of the lines found in the
|
||||
/// scripts referred to by <paramref name="yarnProjectImporter"/> that
|
||||
/// contain any metadata associated with them.
|
||||
/// </summary>
|
||||
/// <param name="yarnProjectImporter">The YarnProjectImporter to extract
|
||||
/// strings from.</param>
|
||||
/// <param name="destination">The path to write the file to.</param>
|
||||
/// <returns><see langword="true"/> if the file was written
|
||||
/// successfully, <see langword="false"/> otherwise.</returns>
|
||||
/// <exception cref="CsvHelper.CsvHelperException">Thrown when an error
|
||||
/// is encountered when generating the CSV data.</exception>
|
||||
/// <exception cref="IOException">Thrown when an error is encountered
|
||||
/// when writing the data to disk.</exception>
|
||||
internal static bool WriteMetadataFile(string destination, YarnProjectImporter yarnProjectImporter)
|
||||
{
|
||||
var lineMetadataEntries = yarnProjectImporter.GenerateLineMetadataEntries();
|
||||
|
||||
// If there was an error, bail out here.
|
||||
if (lineMetadataEntries == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var outputCSV = LineMetadataTableEntry.CreateCSV(lineMetadataEntries);
|
||||
File.WriteAllText(destination, outputCSV);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades an old-style Yarn Project to JSON.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method copies the text of the project to a new file adjacent to
|
||||
/// the project, and replaces the text of the project with a new empty
|
||||
/// JSON project.
|
||||
/// </remarks>
|
||||
/// <param name="importer">A YarnProjectImporter that represents the
|
||||
/// project that needs to be upgraded.</param>
|
||||
internal static void UpgradeYarnProject(YarnProjectImporter importer)
|
||||
{
|
||||
// We need to copy out the variable declarations from the old Yarn
|
||||
// project before we replace it.
|
||||
|
||||
// Get the current text of the old project
|
||||
var existingText = File.ReadAllText(importer.assetPath);
|
||||
|
||||
// Does the existing text contain anything besides the default?
|
||||
var defaultProjectPattern = new System.Text.RegularExpressions.Regex(@"^title:.*?\n---[\n\s]*===[\n\s]*$", System.Text.RegularExpressions.RegexOptions.Multiline);
|
||||
if (defaultProjectPattern.IsMatch(existingText))
|
||||
{
|
||||
// The project contains no content, so there's no need to copy
|
||||
// it out.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a unique path to store our variables
|
||||
var newFilePath = Path.GetDirectoryName(importer.assetPath) + "/Variables.yarn";
|
||||
newFilePath = AssetDatabase.GenerateUniqueAssetPath(newFilePath);
|
||||
|
||||
// Write it out to the new file
|
||||
File.WriteAllText(newFilePath, existingText);
|
||||
}
|
||||
|
||||
// Next, replace the existing project with a new one!
|
||||
var newProject = YarnProjectUtility.CreateDefaultYarnProject();
|
||||
File.WriteAllText(importer.assetPath, newProject.GetJson());
|
||||
|
||||
// Finally, import the assets we've touched.
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
[OnOpenAsset(OnOpenAssetAttributeMode.Execute)]
|
||||
public static bool OnOpenAsset(EntityId instanceID)
|
||||
{
|
||||
|
||||
// temporarily disabling the obsolete warning for the GetAssetPath call
|
||||
// but only for Unity <6.4, otherwise we want the warning
|
||||
// because this code needs to exist across multiple unity versions
|
||||
// and it's only an actual concern on 6.4+ we can disable it when earlier
|
||||
#if !UNITY_6000_4_OR_NEWER
|
||||
#pragma warning disable 0618
|
||||
#endif
|
||||
var path = AssetDatabase.GetAssetPath(instanceID);
|
||||
|
||||
#if !UNITY_6000_4_OR_NEWER
|
||||
#pragma warning restore 0618
|
||||
#endif
|
||||
|
||||
var project = AssetDatabase.LoadAssetAtPath<YarnProject>(path);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var importer = AssetImporter.GetAtPath(path) as Yarn.Unity.Editor.YarnProjectImporter;
|
||||
if (importer == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var yp = Yarn.Compiler.Project.LoadFromFile(path);
|
||||
var files = yp.SourceFiles;
|
||||
|
||||
if (files.Any())
|
||||
{
|
||||
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(System.IO.Path.GetDirectoryName(files.First()), 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[OnOpenAsset(OnOpenAssetAttributeMode.Validate)]
|
||||
public static bool OnValidateAsset(EntityId instanceID)
|
||||
{
|
||||
// temporarily disabling the obsolete warning for the GetAssetPath call
|
||||
// but only for Unity <6.4, otherwise we want the warning
|
||||
// because this code needs to exist across multiple unity versions
|
||||
// and it's only an actual concern on 6.4+ we can disable it when earlier
|
||||
#if !UNITY_6000_4_OR_NEWER
|
||||
#pragma warning disable 0618
|
||||
#endif
|
||||
var path = AssetDatabase.GetAssetPath(instanceID);
|
||||
|
||||
#if !UNITY_6000_4_OR_NEWER
|
||||
#pragma warning restore 0618
|
||||
#endif
|
||||
var project = AssetDatabase.LoadAssetAtPath<YarnProject>(path);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c10d39382c214ee88bd4fd359feb40a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user