This commit is contained in:
SoulliesOfficial
2026-06-09 11:21:59 -04:00
parent 7c60c40d6b
commit 021e76efe7
493 changed files with 50500 additions and 2211 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 991bb6a4238c84092a59ac23d54fda95
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ad1e0218baeab43be922ae40dbfb3ade
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Linq;
using UnityEditor;
using UnityEngine;
using Yarn.Unity;
namespace Yarn.Unity.Editor
{
public static class ActionSourceCodeGenerator
{
/// <summary>
/// Get a path in the current project that can be used for storing
/// manually-generated Yarn Action registration code.
/// </summary>
/// <remarks>
/// This property checks to see if a file exists in the Assets folder
/// that is both named "YarnActionRegistration.cs", and contains a
/// marker indicating that it was generated by Yarn Spinner's code
/// generation systems. If this is found, the path to the file is
/// returned. Otherwise, the path
/// <c>Assets/YarnActionRegistration.cs</c> is returned.
/// </remarks>
public static string GeneratedSourcePath
{
get
{
const string YarnRegistrationFileName = "YarnActionRegistration.cs";
const string DefaultOutputFilePath = "Assets/" + YarnRegistrationFileName;
// Note the lack of a closing parenthesis in this string - we
// only want to check to see if it was generated by
// "YarnActionAnalyzer", not any specific version of that
// analyzer
const string YarnGeneratedCodeSignature = "GeneratedCode(\"YarnActionAnalyzer\"";
var existingFile = System.IO.Directory.EnumerateFiles(System.Environment.CurrentDirectory, YarnRegistrationFileName, System.IO.SearchOption.AllDirectories).FirstOrDefault();
if (existingFile == null)
{
return DefaultOutputFilePath;
}
else
{
try
{
var text = System.IO.File.ReadAllText(existingFile);
return text.Contains(YarnGeneratedCodeSignature)
? existingFile
: DefaultOutputFilePath;
}
catch (System.Exception e)
{
// Something happened while checking the file. Return
// our default, and log that we encountered a problem.
Debug.LogWarning($"Can't check to see if {existingFile} is a valid action registration script, using {DefaultOutputFilePath} instead: {e}");
return DefaultOutputFilePath;
}
}
}
}
/// <summary>
/// Generates and imports a C# source code file in the project
/// containing Yarn Action registration code at the path indicated by
/// <see cref="GeneratedSourcePath"/>.
/// </summary>
/// <remarks>
/// This method should not be called in projects where Unity has support
/// for source generators (i.e. Unity 2021.2 and later).
/// </remarks>
public static void GenerateYarnActionSourceCode()
{
var analysis = new Yarn.Unity.ActionAnalyser.Analyser("Assets");
try
{
var actions = analysis.GetActions();
var source = Yarn.Unity.ActionAnalyser.Analyser.GenerateRegistrationFileSource(actions);
var path = GeneratedSourcePath;
System.IO.File.WriteAllText(path, source);
UnityEditor.AssetDatabase.ImportAsset(path);
Debug.Log($"Generated Yarn command and function registration code at {path}");
}
catch (Yarn.Unity.ActionAnalyser.AnalyserException e)
{
Debug.LogError($"Error generating source code: " + e.InnerException.ToString());
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dabf0558de47a486199baa9e568783ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,625 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Yarn.Unity.ActionAnalyser;
using YarnAction = Yarn.Unity.ActionAnalyser.Action;
#nullable enable
[Generator]
public class ActionRegistrationSourceGenerator : ISourceGenerator
{
const string YarnSpinnerUnityAssemblyName = "YarnSpinner.Unity";
const string DebugLoggingPreprocessorSymbol = "YARN_SOURCE_GENERATION_DEBUG_LOGGING";
const string IncludeTestCommands = "YARN_INCLUDE_TEST_COMMANDS";
const string MinimumUnityVersionPreprocessorSymbol = "UNITY_2021_2_OR_NEWER";
public static string? GetProjectRoot(GeneratorExecutionContext context)
{
// We need to know if the settings are configured to not perform codegen
// to link attributed methods. This is kinda annoying because the path
// root of the project settings and the root path of this process are
// *very* different. So, what we do is we use the included Compilation
// Assembly additional file that Unity gives us. This file, if opened,
// has the path of the Unity project, which we can then use to get the
// settings. If any stage of this fails, then we bail out and assume
// that codegen is desired.
// Try and find any additional files passed to the context
if (!context.AdditionalFiles.Any())
{
return null;
}
// One of those files is (AssemblyName).[Unity]AdditionalFile.txt, and it
// contains the path to the project
var relevantFiles = context.AdditionalFiles.Where(
i => i.Path.Contains($"{context.Compilation.AssemblyName}.AdditionalFile.txt")
|| i.Path.Contains($"{context.Compilation.AssemblyName}.UnityAdditionalFile.txt")
);
if (!relevantFiles.Any())
{
return null;
}
var assemblyRelevantFile = relevantFiles.First();
// The file needs to exist on disk
if (!File.Exists(assemblyRelevantFile.Path))
{
return null;
}
try
{
// Attempt to read it - it should contain the path to the project directory
var projectPath = File.ReadAllText(assemblyRelevantFile.Path);
if (Directory.Exists(projectPath))
{
// If this directory exists, we're done
return projectPath;
}
else
{
return null;
}
}
catch (IOException)
{
// We encountered a problem while testing
return null;
}
}
public void Execute(GeneratorExecutionContext context)
{
using var output = GetOutput(context);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
output.WriteLine(DateTime.Now);
Yarn.Unity.Editor.YarnSpinnerProjectSettings? settings = null;
var projectPath = GetProjectRoot(context);
if (projectPath != null)
{
try
{
var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerProjectSettingsPath);
output.WriteLine($"Attempting to read settings file at {fullPath}");
settings = Yarn.Unity.Editor.YarnSpinnerProjectSettings.GetOrCreateSettings(projectPath, output);
if (!settings.automaticallyLinkAttributedYarnCommandsAndFunctions)
{
output.WriteLine("Skipping codegen due to settings.");
return;
}
}
catch (Exception e)
{
output.WriteLine($"Unable to determine Yarn settings, settings values will be ignored and codegen will occur: {e.Message}");
}
}
else
{
output.WriteLine($"Unable to determine project location on disk. Settings values will be ignored and codegen will occur");
}
bool hasCriticalActionErrors = false;
try
{
output.WriteLine("Source code generation for assembly " + context.Compilation.AssemblyName);
if (context.AdditionalFiles.Any())
{
output.WriteLine($"Additional files:");
foreach (var item in context.AdditionalFiles)
{
output.WriteLine(" " + item.Path);
}
}
output.WriteLine("Referenced assemblies for this compilation:");
foreach (var referencedAssembly in context.Compilation.ReferencedAssemblyNames)
{
output.WriteLine(" - " + referencedAssembly.Name);
}
bool compilationReferencesYarnSpinner = context.Compilation.ReferencedAssemblyNames
.Any(name => name.Name == YarnSpinnerUnityAssemblyName);
if (compilationReferencesYarnSpinner == false)
{
// This compilation doesn't reference YarnSpinner.Unity. Any
// code that we generate that references symbols in that
// assembly won't work.
output.WriteLine($"Assembly {context.Compilation.AssemblyName} doesn't reference {YarnSpinnerUnityAssemblyName}. Not generating any code for it.");
return;
}
output.WriteLine("Preprocessor Symbols: ");
foreach (var symbol in context.ParseOptions.PreprocessorSymbolNames)
{
output.WriteLine("- " + symbol);
}
// Don't generate source code if we're not targeting at least Unity
// 2021.2. (Unity will not invoke this DLL as a source code
// generator until at least this version, but other tools like
// OmniSharp might.)
if (!context.ParseOptions.PreprocessorSymbolNames.Contains(MinimumUnityVersionPreprocessorSymbol))
{
output.WriteLine($"Not generating code for assembly {context.Compilation.AssemblyName} because this assembly is not being built for Unity 2021.2 or newer");
return;
}
// Don't generate source code for certain Yarn Spinner provided
// assemblies - these always manually register any actions in them.
var prefixesToIgnore = new List<string>()
{
"YarnSpinner.Unity",
"YarnSpinner.Editor",
};
// But DO generate source code for the Samples assembly and the Test assembly
var prefixesToKeep = new List<string>()
{
"YarnSpinner.Unity.Samples",
};
// Additionally, if we're building for unit tests, include the Yarn
// Spinner unit tests assembly.
if (context.ParseOptions.PreprocessorSymbolNames.Contains(IncludeTestCommands))
{
prefixesToKeep.Add("YarnSpinner.Unity.Tests");
}
if (context.Compilation.AssemblyName == null)
{
output.WriteLine("Not generating registration code, because the provided AssemblyName is null");
return;
}
if (prefixesToIgnore.Any(prefix => context.Compilation.AssemblyName.StartsWith(prefix)) && !prefixesToKeep.Any(prefix => context.Compilation.AssemblyName.StartsWith(prefix)))
{
output.WriteLine($"Not generating registration code for {context.Compilation.AssemblyName}: we've been told to exclude it, because its name begins with one of these prefixes: {string.Join(", ", prefixesToIgnore)}");
return;
}
if (!(context.Compilation is CSharpCompilation compilation))
{
// This is not a C# compilation, so we can't do analysis.
output.WriteLine($"Stopping code generation because compilation is not a {nameof(CSharpCompilation)}.");
return;
}
var actions = new List<YarnAction>();
foreach (var tree in compilation.SyntaxTrees)
{
actions.AddRange(Analyser.GetActions(compilation, tree, output));
}
if (actions.Count() == 0)
{
output.WriteLine($"Didn't find any Yarn Actions in {context.Compilation.AssemblyName}. Not generating any source code for it.");
return;
}
// validating and logging all the actions
foreach (var action in actions)
{
if (action == null)
{
output.WriteLine($"Action is null??");
continue;
}
var diagnostics = action.Validate(compilation, output);
foreach (var diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic);
if (diagnostic.Severity == DiagnosticSeverity.Warning || diagnostic.Severity == DiagnosticSeverity.Error)
{
output.WriteLine($"Flagging '{action.Name}' ({action.MethodName}): {diagnostic}");
action.ContainsErrors = true;
if (diagnostic.Severity == DiagnosticSeverity.Error)
{
hasCriticalActionErrors = true;
}
}
}
// Commands are parsed as whitespace, so spaces in the command name
// would render the command un-callable.
if (action.Name.Any(x => Char.IsWhiteSpace(x)))
{
var descriptor = new DiagnosticDescriptor(
"YS1002",
$"Yarn {action.Type} methods must have a valid name",
"YarnCommand and YarnFunction methods follow existing ID rules for Yarn. \"{0}\" is invalid.",
"Yarn Spinner",
DiagnosticSeverity.Warning,
true,
"[YarnCommand] and [YarnFunction] attributed methods must follow Yarn ID rules so that Yarn scripts can reference them.",
"https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
context.ReportDiagnostic(Microsoft.CodeAnalysis.Diagnostic.Create(
descriptor,
action.Declaration?.GetLocation(),
action.Name
));
action.ContainsErrors = true;
output.WriteLine($"Action {action.MethodIdentifierName} will be flagged due to it's name {action.Name}");
continue;
}
output.WriteLine($"Action {action.Name}: {action.SourceFileName}:{action.Declaration?.GetLocation()?.GetLineSpan().StartLinePosition.Line} ({action.Type})");
}
if (hasCriticalActionErrors)
{
stopwatch.Stop();
output.WriteLine($"Critical issues were encountered in the actions, aborting code generation, stopping analysis after {stopwatch.Elapsed.TotalMilliseconds}ms");
return;
}
output.Write($"Generating source code...");
var source = Analyser.GenerateRegistrationFileSource(actions);
output.WriteLine($"Done.");
SourceText sourceText = SourceText.From(source, Encoding.UTF8);
output.Write($"Writing generated source...");
DumpGeneratedFile(context, source);
output.WriteLine($"Done.");
context.AddSource($"YarnActionRegistration-{compilation.AssemblyName}.Generated.cs", sourceText);
if (settings != null)
{
if (settings.generateYSLSFile)
{
output.Write($"Generating ysls...");
// generating the ysls
IEnumerable<string> commandJSON = actions.Where(a => a.Type == ActionType.Command).Select(a => a.ToJSON());
IEnumerable<string> functionJSON = actions.Where(a => a.Type == ActionType.Function).Select(a => a.ToJSON());
var ysls = "{" +
@"""version"":2," +
$@"""commands"":[{string.Join(",", commandJSON)}]," +
$@"""functions"":[{string.Join(",", functionJSON)}]" +
"}";
output.WriteLine($"Done.");
if (!string.IsNullOrEmpty(projectPath))
{
output.Write($"Writing generated ysls...");
var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerAssemblyGeneratedYSLSPath(compilation.AssemblyName));
try
{
System.IO.File.WriteAllText(fullPath, ysls);
output.WriteLine($"Done.");
}
catch (Exception e)
{
output.WriteLine($"Unable to write ysls to disk: {e.Message}");
}
}
else
{
output.WriteLine("unable to identify project path, ysls will not be written to disk");
}
}
else
{
output.WriteLine($"skipping ysls generation due to settings");
}
}
else
{
output.WriteLine($"skipping ysls generation due to settings not being found");
}
stopwatch.Stop();
output.WriteLine($"Source code generation completed in {stopwatch.Elapsed.TotalMilliseconds}ms");
return;
}
catch (Exception e)
{
output.WriteLine($"{e}");
}
}
private MethodDeclarationSyntax GenerateLoggingMethod(string methodName, string sourceExpression, string prefix)
{
return SyntaxFactory.MethodDeclaration(
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.VoidKeyword)),
SyntaxFactory.Identifier(methodName))
.WithModifiers(
SyntaxFactory.TokenList(
new[]{
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword)}))
.WithBody(
SyntaxFactory.Block(
SyntaxFactory.LocalDeclarationStatement(
SyntaxFactory.VariableDeclaration(
SyntaxFactory.GenericName(
SyntaxFactory.Identifier("IEnumerable"))
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList<TypeSyntax>(
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.StringKeyword))))))
.WithVariables(
SyntaxFactory.SingletonSeparatedList<VariableDeclaratorSyntax>(
SyntaxFactory.VariableDeclarator(
SyntaxFactory.Identifier("source"))
.WithInitializer(
SyntaxFactory.EqualsValueClause(
SyntaxFactory.ParseExpression(sourceExpression)))))),
SyntaxFactory.LocalDeclarationStatement(
SyntaxFactory.VariableDeclaration(
SyntaxFactory.IdentifierName(
SyntaxFactory.Identifier(
SyntaxFactory.TriviaList(),
SyntaxKind.VarKeyword,
"var",
"var",
SyntaxFactory.TriviaList())))
.WithVariables(
SyntaxFactory.SingletonSeparatedList<VariableDeclaratorSyntax>(
SyntaxFactory.VariableDeclarator(
SyntaxFactory.Identifier("prefix"))
.WithInitializer(
SyntaxFactory.EqualsValueClause(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(prefix))))))),
SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("Debug"),
SyntaxFactory.IdentifierName("Log")
)
)
.WithArgumentList(
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList<ArgumentSyntax>(
SyntaxFactory.Argument(
SyntaxFactory.InterpolatedStringExpression(
SyntaxFactory.Token(SyntaxKind.InterpolatedVerbatimStringStartToken)
)
.WithContents(
SyntaxFactory.List<InterpolatedStringContentSyntax>(
new InterpolatedStringContentSyntax[]{
SyntaxFactory.Interpolation(
SyntaxFactory.IdentifierName("prefix")
),
SyntaxFactory.InterpolatedStringText()
.WithTextToken(
SyntaxFactory.Token(
SyntaxFactory.TriviaList(),
SyntaxKind.InterpolatedStringTextToken,
" ",
" ",
SyntaxFactory.TriviaList()
)
),
SyntaxFactory.Interpolation(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.StringKeyword)
),
SyntaxFactory.IdentifierName("Join")
)
)
.WithArgumentList(
SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList<ArgumentSyntax>(
new SyntaxNodeOrToken[]{
SyntaxFactory.Argument(
SyntaxFactory.LiteralExpression(
SyntaxKind.CharacterLiteralExpression,
SyntaxFactory.Literal(';')
)
),
SyntaxFactory.Token(SyntaxKind.CommaToken),
SyntaxFactory.Argument(
SyntaxFactory.IdentifierName("source")
)
}
)
)
)
)
}
)
)
)
)
)
)
)
)
)
.NormalizeWhitespace();
}
public static MethodDeclarationSyntax GenerateSingleLogMethod(string methodName, string text, string prefix)
{
return SyntaxFactory.MethodDeclaration(
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.VoidKeyword)
),
SyntaxFactory.Identifier(methodName)
)
.WithModifiers(
SyntaxFactory.TokenList(
new[]{
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword)
}
)
)
.WithBody(
SyntaxFactory.Block(
SyntaxFactory.SingletonList<StatementSyntax>(
SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("Debug"),
SyntaxFactory.IdentifierName("Log")
)
)
.WithArgumentList(
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList<ArgumentSyntax>(
SyntaxFactory.Argument(
SyntaxFactory.InterpolatedStringExpression(
SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken)
)
.WithContents(
SyntaxFactory.List<InterpolatedStringContentSyntax>(
new InterpolatedStringContentSyntax[]{
SyntaxFactory.Interpolation(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(prefix)
)
),
SyntaxFactory.InterpolatedStringText()
.WithTextToken(
SyntaxFactory.Token(
SyntaxFactory.TriviaList(),
SyntaxKind.InterpolatedStringTextToken,
" ",
" ",
SyntaxFactory.TriviaList()
)
),
SyntaxFactory.Interpolation(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(text)
)
)
}
)
)
)
)
)
)
)
)
)
)
.NormalizeWhitespace();
}
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new ClassDeclarationSyntaxReceiver());
}
static string GetTemporaryPath(GeneratorExecutionContext context)
{
string tempPath;
var rootPath = GetProjectRoot(context);
if (rootPath != null)
{
tempPath = Path.Combine(rootPath, "Logs", "Packages", "dev.yarnspinner.unity");
}
else
{
tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "dev.yarnspinner.logs");
}
// we need to make the logs folder, but this can potentially fail
// if it does fail then we will just chuck the logs inside the tmp folder
try
{
if (!Directory.Exists(tempPath))
{
Directory.CreateDirectory(tempPath);
}
}
catch
{
tempPath = System.IO.Path.GetTempPath();
}
return tempPath;
}
public Yarn.Unity.ILogger GetOutput(GeneratorExecutionContext context)
{
if (GetShouldLogToFile(context))
{
var tempPath = ActionRegistrationSourceGenerator.GetTemporaryPath(context);
var path = System.IO.Path.Combine(tempPath, $"{nameof(ActionRegistrationSourceGenerator)}-{context.Compilation.AssemblyName}.txt");
var outFile = System.IO.File.Open(path, System.IO.FileMode.Create);
return new Yarn.Unity.FileLogger(new System.IO.StreamWriter(outFile));
}
else
{
return new Yarn.Unity.NullLogger();
}
}
private static bool GetShouldLogToFile(GeneratorExecutionContext context)
{
return context.ParseOptions.PreprocessorSymbolNames.Contains(DebugLoggingPreprocessorSymbol);
}
public void DumpGeneratedFile(GeneratorExecutionContext context, string text)
{
if (GetShouldLogToFile(context))
{
var tempPath = ActionRegistrationSourceGenerator.GetTemporaryPath(context);
var path = System.IO.Path.Combine(tempPath, $"{nameof(ActionRegistrationSourceGenerator)}-{context.Compilation.AssemblyName}.cs");
System.IO.File.WriteAllText(path, text);
}
}
}
internal class ClassDeclarationSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> Classes { get; private set; } = new List<ClassDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// Business logic to decide what we're interested in goes here
if (syntaxNode is ClassDeclarationSyntax cds)
{
Classes.Add(cds);
}
}
}

View File

@@ -0,0 +1,9 @@
# Actions Registration Generator for Yarn Spinner
This folder contains the source code for the [source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) that produces registration code for Yarn Spinner actions (for example, `YarnCommand` and `YarnAction`).
This folder ends with a tilde `~` to make Unity not aware of it. To build the source code generator, install the .NET SDK and use `dotnet-build`. The built DLL will be placed at the following path:
```
(path to Yarn Spinner)/SourceGenerator/YarnSpinner.Unity.SourceCodeGenerator.dll
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee966ea0c2700450080c5d533c3c5afc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Collections.Generic;
namespace Yarn.Unity.ActionAnalyser
{
[System.Serializable]
public class AnalyserException : System.Exception
{
public AnalyserException() { }
public AnalyserException(string message) : base(message) { }
public AnalyserException(string message, System.Exception inner) : base(message, inner) { }
public AnalyserException(string message, System.Exception inner, IEnumerable<Microsoft.CodeAnalysis.Diagnostic> diagnostics) : base(message, inner)
{
this.Diagnostics = diagnostics;
}
protected AnalyserException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
public IEnumerable<Microsoft.CodeAnalysis.Diagnostic> Diagnostics { get; } = new List<Microsoft.CodeAnalysis.Diagnostic>();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4318b63e7e2584e3895f7b9c8782154b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 38292618b0e0e46e2a23aa0b7ab05be9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 4e596e8d36f4949e49a276da7dcf74d0
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: d0d5eb061e5a94e61918a4bcc8343d0f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 05b4f6ee4402b4befa8b93e0b0f30072
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: d36ed78c475f14eada798a8881d84f4a
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: f124754956f07408dafc0d384a437712
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 33c2c99bdbd9f48f1bfe7333e3c7f401
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 8807786a5b21e4484ae712f0f92620db
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 6b1c9dc69df6f47a38bd1ad37d4319bd
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,111 @@
using Microsoft.CodeAnalysis;
public static class Diagnostics
{
public static readonly DiagnosticDescriptor YS1000UnknownError = new DiagnosticDescriptor(
"YS0000",
title: $"Internal unknown error",
messageFormat: "An internal error was encountered while processing this action: {0}",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor YS1001ActionMethodsMustBePublic = new DiagnosticDescriptor(
"YS1001",
title: $"Yarn action methods must be public",
messageFormat: "YarnCommand and YarnFunction methods must be public. \"{0}\" is {1}.",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "[YarnCommand] and [YarnFunction] attributed methods must be public so that the codegen can reference them.",
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1002ActionMethodsMustHaveAValidName = new DiagnosticDescriptor(
"YS1002",
title: $"Yarn action methods must have a valid name",
messageFormat: "YarnCommand and YarnFunction methods must follow existing ID rules for Yarn. \"{0}\" is invalid.",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "[YarnCommand] and [YarnFunction] attributed methods must follow Yarn ID rules so that Yarn scripts can reference them.",
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1003CommandMethodsMustHaveAValidReturnType = new DiagnosticDescriptor(
"YS1003",
title: $"YarnCommand methods must return a valid type",
messageFormat: $"YarnCommand methods must return a valid type (either void, a coroutine, or a task). \"{{0}}\"'s return type is {{1}}.",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1004FunctionMethodsMustHaveAValidReturnType = new DiagnosticDescriptor(
"YS1004",
title: $"YarnFunction methods must return a valid type",
messageFormat: $"YarnFunction methods must return a valid type (either bool, string, or a numeric type). \"{{0}}\"'s return type is {{1}}.",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1005ActionMethodsMustHaveOneActionAttribute = new DiagnosticDescriptor(
"YS1005",
title: $"Yarn action methods must have a single YarnCommand or YarnAction attribute",
messageFormat: $"YarnCommand and YarnFunction methods must have a single attribute. \"{{0}}\" has {{1}}.",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1006YarnFunctionsMustBeStatic = new DiagnosticDescriptor(
"YS1006",
title: $"YarnFunction methods be static",
messageFormat: $"YarnFunction methods are required to be static.",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1008ActionsParamsArraysMustBeOfYarnTypes = new DiagnosticDescriptor(
"YS1008",
title: "Params arrays must be of a Yarn compatible type",
messageFormat: "Params arrays must be of a Yarn compatible type, but {0} is of type \"{1}\"",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1009ActionsEnumAttributedParameterIsOfIncompatibleType = new DiagnosticDescriptor(
"YS1009",
title: "Yarn Enum attributed parameters must be of a Yarn compatible type",
messageFormat: "Yarn Enum attributed parameters must be of a Yarn compatible type, but {0} is of type \"{1}\"",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1010ActionsNodeAttributedParameterIsOfIncompatibleType = new DiagnosticDescriptor(
"YS1010",
title: "Yarn Node attributed parameters must be a string",
messageFormat: "Yarn Node attributed parameters must be a string, but {0} is of type \"{1}\"",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1011ActionsParameterIsAnIncompatibleType = new DiagnosticDescriptor(
"YS1011",
title: "Yarn action parameters must be of a Yarn compatible type",
messageFormat: "Yarn action parameters must be of a Yarn compatible type, but {0} is of type \"{1}\"",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
public static readonly DiagnosticDescriptor YS1012ActionIsALambda = new DiagnosticDescriptor(
"YS1012",
title: "Yarn actions can be lambdas but this generally isn't recommended",
messageFormat: "Yarn actions can be lambdas but this generally isn't recommended. Lambda based actions cannot be unregistered and are more difficult to debug",
category: "Yarn Spinner",
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ef837099f8664a9ab951ed2c6ba6e72
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System;
using System.Collections.Generic;
#nullable enable
static class EnumerableExtensions
{
public static IEnumerable<T> NonNull<T>(this IEnumerable<T?> collection, bool throwIfAnyNull = false) where T : class
{
foreach (var item in collection)
{
if (item != null)
{
yield return item;
}
else
{
if (throwIfAnyNull)
{
throw new NullReferenceException("Collection contains a null item");
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a3d39b6f28ae4a31989cb67e5aa3d98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,132 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
#endif
namespace Yarn.Unity
{
#nullable enable
public interface ILogger : IDisposable
{
void Write(object obj);
void WriteLine(object obj);
void WriteException(System.Exception ex, string? message = null);
void Inc();
void Dec();
void SetDepth(int depth);
}
public class FileLogger : ILogger
{
System.IO.TextWriter writer;
private int depth = 0;
public FileLogger(System.IO.TextWriter writer)
{
this.writer = writer;
}
public void Dispose()
{
writer.Flush();
writer.Dispose();
}
public void Write(object text)
{
var tabs = new String('\t', depth);
writer.Write(tabs + text);
}
public void WriteLine(object text)
{
var tabs = new String('\t', depth);
writer.WriteLine(tabs + text);
}
public void WriteException(System.Exception ex, string? message)
{
var tabs = new String('\t', depth);
if (message == null)
{
writer.WriteLine($"{tabs}Exception: {ex.Message}");
}
else
{
writer.WriteLine($"{tabs}{message}: {ex.Message}");
}
}
public void Inc()
{
depth +=1 ;
}
public void Dec()
{
depth = Math.Max(depth - 1, 0);
}
public void SetDepth(int depth)
{
this.depth = Math.Max(depth, 0);
}
}
public class UnityLogger : ILogger
{
public void Dispose() { }
public void Write(object text)
{
WriteLine(text);
}
public void WriteLine(object text)
{
var tabs = new String('\t', depth);
#if UNITY_EDITOR
Debug.LogWarning(tabs + text.ToString());
#endif
}
public void WriteException(System.Exception ex, string? message = null)
{
#if UNITY_EDITOR
Debug.LogException(ex);
#endif
}
private int depth = 0;
public void Inc()
{
depth +=1 ;
}
public void Dec()
{
depth = Math.Max(depth - 1, 0);
}
public void SetDepth(int depth)
{
this.depth = Math.Max(depth, 0);
}
}
public class NullLogger : ILogger
{
public void Dispose() { }
public void Write(object text) { }
public void WriteLine(object text) { }
public void WriteException(System.Exception ex, string? message = null) { }
public void Inc(){}
public void Dec(){}
public void SetDepth(int depth) {}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45e3ce698d3364e5790443887a66daff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using Microsoft.CodeAnalysis;
using System.Linq;
#nullable enable
static class SymbolExtensions
{
/// <summary>
/// If the <paramref name="symbol"/> is a method symbol, returns <see langword="true"/> if the method's return type is "awaitable", but not if it's <see langword="dynamic"/>.
/// If the <paramref name="symbol"/> is a type symbol, returns <see langword="true"/> if that type is "awaitable".
/// An "awaitable" is any type that exposes a GetAwaiter method which returns a valid "awaiter". This GetAwaiter method may be an instance method or an extension method.
/// </summary>
public static bool IsAwaitableNonDynamic(this ISymbol symbol, SemanticModel semanticModel, int position)
{
IMethodSymbol? methodSymbol = symbol as IMethodSymbol;
ITypeSymbol? typeSymbol = null;
if (methodSymbol == null)
{
typeSymbol = symbol as ITypeSymbol;
if (typeSymbol == null)
{
return false;
}
}
else
{
if (methodSymbol.ReturnType == null)
{
return false;
}
}
// otherwise: needs valid GetAwaiter
var potentialGetAwaiters = semanticModel.LookupSymbols(position,
container: typeSymbol ?? methodSymbol?.ReturnType.OriginalDefinition,
name: WellKnownMemberNames.GetAwaiter,
includeReducedExtensionMethods: true);
var getAwaiters = potentialGetAwaiters.OfType<IMethodSymbol>().Where(x => !x.Parameters.Any());
return getAwaiters.Any(VerifyGetAwaiter);
}
private static bool VerifyGetAwaiter(IMethodSymbol getAwaiter)
{
var returnType = getAwaiter.ReturnType;
if (returnType == null)
{
return false;
}
// bool IsCompleted { get }
if (!returnType.GetMembers().OfType<IPropertySymbol>().Any(p => p.Name == WellKnownMemberNames.IsCompleted && p.Type.SpecialType == SpecialType.System_Boolean && p.GetMethod != null))
{
return false;
}
var methods = returnType.GetMembers().OfType<IMethodSymbol>();
// NOTE: (vladres) The current version of C# Spec, §7.7.7.3 'Runtime evaluation of await expressions', requires that
// NOTE: the interface method INotifyCompletion.OnCompleted or ICriticalNotifyCompletion.UnsafeOnCompleted is invoked
// NOTE: (rather than any OnCompleted method conforming to a certain pattern).
// NOTE: Should this code be updated to match the spec?
// void OnCompleted(Action)
// Actions are delegates, so we'll just check for delegates.
if (!methods.Any(x => x.Name == WellKnownMemberNames.OnCompleted && x.ReturnsVoid && x.Parameters.Length == 1 && x.Parameters.First().Type.TypeKind == TypeKind.Delegate))
{
return false;
}
// void GetResult() || T GetResult()
return methods.Any(m => m.Name == WellKnownMemberNames.GetResult && !m.Parameters.Any());
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 245397f2263474149856dba1cfc50f19
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: AssemblyVersion("3.1.4.0")]
[assembly: AssemblyFileVersion("3.1.4.0")]
[assembly: AssemblyInformationalVersion("3.1.4.Branch.hotfix/textanim-build-errors.Sha.c2b119c5eda7fdd3cd0b13a689f95d54d456fb69")]
[assembly: InternalsVisibleTo("YarnSpinner.Unity.Editor")]
[assembly: InternalsVisibleTo("YarnSpinner.Unity.Tests.Editor")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be2fc8d5bc6c7624e95557f8af15a1b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 53fbdca23523543cfbcda84a57c09996
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,243 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
#nullable enable
namespace Yarn.Unity.Editor
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.AssetImporters;
using UnityEditorInternal;
using UnityEngine;
using Yarn.Compiler;
#if USE_UNITY_LOCALIZATION
using UnityEditor.Localization;
using UnityEngine.Localization.Tables;
using UnityEngine.Localization;
public class AddAssetsToAssetTableCollectionWizard : EditorWindow
{
private YarnProject? yarnProject;
private AssetTableCollection? assetTableCollection;
private Type? _assetType;
private Type AssetType
{
get
{
if (_assetType == null)
{
var typeName = EditorPrefs.GetString("YarnSpinner_AddAssets_AssetType", string.Empty);
if (typeName != string.Empty)
{
_assetType = System.Type.GetType(typeName, throwOnError: false, ignoreCase: false);
}
}
_assetType ??= typeof(AudioClip);
return _assetType;
}
set
{
_assetType = value;
EditorPrefs.SetString("YarnSpinner_AddAssets_AssetType", _assetType?.AssemblyQualifiedName ?? string.Empty);
}
}
private Dictionary<string, DefaultAsset?> localesToFolders = new Dictionary<string, DefaultAsset?>();
private Type[] allTypes = Array.Empty<Type>();
private GUIContent[] allTypeContents = Array.Empty<GUIContent>();
System.Collections.ObjectModel.ReadOnlyCollection<UnityEngine.Localization.Locale>? locales = null;
private Dictionary<LocaleIdentifier, LocalizationTable> _cachedTables = new();
const string description = "This tool searches Asset Folders for assets of the selected type, and then adds them to an Asset Table Collection. Line Providers, like Unity Localised Line Provider, can then fetch these assets at run-time.";
[MenuItem("Window/Yarn Spinner/Add Assets to Table Collection")]
static void Init()
{
// Get existing open window or if none, make a new one:
var window = CreateWindow<AddAssetsToAssetTableCollectionWizard>();
window.ShowPopup();
window.titleContent = new GUIContent("Add Assets to Table Collection");
if (Selection.activeObject is AssetTableCollection collection)
{
window.assetTableCollection = collection;
}
}
void OnEnable()
{
allTypes = TypeCache.GetTypesDerivedFrom<UnityEngine.Object>().OrderBy(t => t.FullName).ToArray();
allTypeContents = allTypes.Select(t => new GUIContent(
t.FullName.Replace(".", "/"),
EditorGUIUtility.ObjectContent(null, t).image
)).ToArray();
locales = LocalizationEditorSettings.GetLocales();
}
void OnGUI()
{
using (new GUILayout.VerticalScope())
{
EditorGUILayout.BeginVertical(EditorStyles.inspectorFullWidthMargins);
EditorGUILayout.HelpBox(description, MessageType.Info);
assetTableCollection = EditorGUILayout.ObjectField(new GUIContent("Asset Table Collection", "The asset table collection to add assets to"), assetTableCollection, typeof(AssetTableCollection), allowSceneObjects: false) as AssetTableCollection;
yarnProject = EditorGUILayout.ObjectField(new GUIContent("Yarn Project", "The Yarn Project to add assets for."), yarnProject, typeof(YarnProject), allowSceneObjects: false) as YarnProject;
var selectedIndex = Array.IndexOf(allTypes, AssetType);
using (var change = new EditorGUI.ChangeCheckScope())
{
var newSelection = EditorGUILayout.Popup(new GUIContent("Asset Type", "The type of assets to find in the Asset Folders"), selectedIndex, allTypeContents);
if (change.changed)
{
AssetType = allTypes[newSelection];
}
}
var headerStyle = EditorStyles.boldLabel;
if (assetTableCollection != null)
{
EditorGUILayout.LabelField("Asset Folders", headerStyle);
EditorGUI.indentLevel += 1;
if (locales == null)
{
EditorGUILayout.HelpBox("No locales were found in your project. Is Unity Localization installed and correctly configured?", MessageType.Error);
return;
}
foreach (var locale in locales)
{
if (_cachedTables.TryGetValue(locale.Identifier, out var localizationTable) == false || localizationTable == null)
{
if (assetTableCollection != null)
{
localizationTable = assetTableCollection.GetTable(locale.Identifier);
_cachedTables[locale.Identifier] = localizationTable;
}
}
if (localizationTable == null)
{
EditorGUILayout.LabelField(new GUIContent(locale.LocaleName), new GUIContent("No table in collection"));
continue;
}
using (new EditorGUI.DisabledGroupScope(localizationTable == null))
{
localesToFolders.TryGetValue(locale.Identifier.Code, out var currentFolder);
localesToFolders[locale.Identifier.Code] = EditorGUILayout.ObjectField(
new GUIContent(locale.LocaleName, $"The folder to search for {AssetType.Name} assets for the '{locale.LocaleName}' locale"),
currentFolder,
typeof(DefaultAsset),
allowSceneObjects: false) as DefaultAsset;
}
}
EditorGUI.indentLevel -= 1;
}
GUILayout.FlexibleSpace();
using (new GUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
var readyToAddAssets = assetTableCollection != null && yarnProject != null && localesToFolders.Where(kv => kv.Value != null).Count() > 0;
using (new EditorGUI.DisabledGroupScope(!readyToAddAssets))
{
if (GUILayout.Button("Add Assets"))
{
AddAssets(assetTableCollection!, yarnProject!, localesToFolders!, AssetType);
// this.Close();
}
}
}
EditorGUILayout.EndVertical();
}
}
private static void AddAssets(AssetTableCollection assetTableCollection, YarnProject yarnProject, IReadOnlyDictionary<string, DefaultAsset> localesToFolders, System.Type assetType)
{
var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(yarnProject)) as YarnProjectImporter;
if (importer == null)
{
throw new InvalidOperationException("Failed to get an importer for Yarn Project");
}
var stringTable = importer.GenerateStringsTable();
var lineIDs = stringTable.Select(e => e.ID);
int totalCount = 0;
foreach (var locale in LocalizationEditorSettings.GetLocales())
{
int perLocaleCount = 0;
if (localesToFolders.TryGetValue(locale.Identifier.Code, out var folder) == false
|| folder == null)
{
// No folder given for this locale. Skip it!
Debug.Log($"Skipping {locale.LocaleName} because no folder was provided");
continue;
}
if (assetTableCollection.ContainsTable(locale.Identifier) == false)
{
// No table in this collection for this locale! Skip it!
Debug.Log($"Skipping {locale.LocaleName} because no table exists");
continue;
}
var path = AssetDatabase.GetAssetPath(folder);
var idsToAssetPaths = YarnProjectUtility.FindAssetPathsForLineIDs(lineIDs, AssetDatabase.GetAssetPath(folder), assetType);
foreach (var (lineID, assetPath) in idsToAssetPaths)
{
var asset = AssetDatabase.LoadAssetAtPath(assetPath, assetType);
if (asset == null)
{
// Not the type of asset we're looking for
continue;
}
assetTableCollection.AddAssetToTable(locale.Identifier, lineID, asset);
perLocaleCount += 1;
totalCount += 1;
}
EditorUtility.SetDirty(assetTableCollection);
Debug.Log($"Added {perLocaleCount} assets to {locale.LocaleName}");
}
Debug.Log($"Added {totalCount} assets to asset table collection");
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2c8a684ed88e47a083d7334e5f24ebc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6042640492df0421da69d5084456a10c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:Label text="Label" display-tooltip-when-elided="true" name="Title" />
<ui:Label text="Label" display-tooltip-when-elided="true" name="Subtitle" />
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 90cfaddaa17c84dc79cd95f4448553b2
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,195 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
#nullable enable
namespace Yarn.Unity.Editor
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Yarn.Unity;
internal class CommandsCollection : IActionRegistration
{
public List<Actions.CommandRegistration> commandRegistrations = new List<Actions.CommandRegistration>();
public List<(string Name, Delegate Function)> functionRegistrations = new List<(string Name, Delegate Function)>();
public IEnumerable<CommandsWindow.IListItem> GetListItems()
{
foreach (var registrationMethod in Actions.ActionRegistrationMethods)
{
registrationMethod.Invoke(this, RegistrationType.Compilation);
}
yield return new CommandsWindow.HeaderListItem("Commands");
foreach (var command in commandRegistrations)
{
yield return new CommandsWindow.CommandListItem(command);
}
// Add a fake 'stop' command to the list, so that it appears in the
// window
System.Action fakeStop = () => { };
yield return new CommandsWindow.CommandListItem(new Actions.CommandRegistration("stop", fakeStop));
}
public void AddCommandHandler(string commandName, Delegate handler)
{
commandRegistrations.Add(new Actions.CommandRegistration(commandName, handler));
}
public void AddCommandHandler(string commandName, MethodInfo methodInfo)
{
commandRegistrations.Add(new Actions.CommandRegistration(commandName, methodInfo));
}
public void AddFunction(string name, Delegate implementation) => functionRegistrations.Add((name, implementation));
public void RemoveCommandHandler(string commandName)
{
// No-op
}
public void RemoveFunction(string name)
{
// No-op
}
public void RegisterFunctionDeclaration(string name, Type returnType, Type[] parameterTypes)
{
/* TODO: Implement */
}
}
public class CommandsWindow : EditorWindow
{
public interface IListItem { string DisplayName { get; } }
public class HeaderListItem : IListItem
{
public string DisplayName { get; set; }
public HeaderListItem(string displayName)
{
DisplayName = displayName;
}
}
public class CommandListItem : IListItem
{
internal Actions.CommandRegistration Command;
internal CommandListItem(Actions.CommandRegistration command)
{
Command = command;
}
public string DisplayName => Command.Name;
}
[SerializeField] private VisualTreeAsset? uxml;
[SerializeField] private VisualTreeAsset? listItemUXML;
[SerializeField] private StyleSheet? stylesheet;
private List<IListItem> listItems = new();
private List<IListItem> filteredListItems = new();
[MenuItem("Window/Yarn Spinner/Commands...")]
static void Summon()
{
var window = GetWindow<CommandsWindow>("Yarn Commands");
window.Show();
}
void CreateGUI()
{
if (uxml == null)
{
Debug.LogWarning($"{GetType()}'s {nameof(uxml)} is null");
return;
}
uxml.CloneTree(rootVisualElement);
var listView = rootVisualElement.Q<ListView>();
var searchField = rootVisualElement.Q<UnityEditor.UIElements.ToolbarSearchField>();
searchField.RegisterValueChangedCallback(evt =>
{
UpdateFilter(listView, searchField.value);
});
var commandsCollection = new CommandsCollection();
listItems = new List<IListItem>(commandsCollection.GetListItems().OrderBy(item => item.DisplayName));
UpdateFilter(listView, searchField.value);
// Set ListView.makeItem to initialize each entry in the list.
listView.makeItem = () =>
{
if (listItemUXML == null)
{
throw new InvalidOperationException($"Can't create new list item: {nameof(listItemUXML)} is null");
}
var result = listItemUXML.CloneTree();
result.styleSheets.Add(stylesheet);
result.AddToClassList("commandListItem");
return result;
};
// Set ListView.bindItem to bind an initialized entry to a data item.
listView.bindItem = (VisualElement element, int index) =>
{
var listItem = filteredListItems[index];
element.Q<Label>("Title").text = listItem.DisplayName;
element.RemoveFromClassList("header");
element.RemoveFromClassList("command");
if (listItem is HeaderListItem)
{
element.AddToClassList("header");
}
else if (listItem is CommandListItem command)
{
element.AddToClassList("command");
element.Q<Label>("Subtitle").text = "<<" + command.Command.UsageString + ">>";
}
};
}
private void UpdateFilter(ListView listView, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
filteredListItems = listItems;
}
else
{
filteredListItems = listItems.Where(item =>
{
if (item is HeaderListItem)
{
return true;
}
if (item is CommandListItem command)
{
return command.Command.Name.IndexOf(value, StringComparison.InvariantCultureIgnoreCase) != -1;
}
return true;
}).ToList();
}
listView.itemsSource = filteredListItems;
}
}
}

View File

@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 280f575059ceb49eabeb009efa8936bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- uxml: {fileID: 9197481963319205126, guid: e71e992188faa4d22b8bdd4b60f0b7bf, type: 3}
- listItemUXML: {fileID: 9197481963319205126, guid: 90cfaddaa17c84dc79cd95f4448553b2, type: 3}
- stylesheet: {fileID: 7433441132597879392, guid: f8653a3e6bb8c4c45aa986ef57baedbb, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
.header #Title {
-unity-font-style: bold;
white-space: normal;
}
.command {
padding-left: 20px;
}
.header #Subtitle {
display: none;
}
.command #Subtitle {
white-space: normal;
padding-left: 20px;
opacity: 0.5;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8653a3e6bb8c4c45aa986ef57baedbb
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<uie:Toolbar>
<uie:ToolbarSearchField focusable="true" style="flex-grow: 1;" />
</uie:Toolbar>
<ui:ListView focusable="true" fixed-item-height="20" selection-type="None" virtualization-method="DynamicHeight" />
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: e71e992188faa4d22b8bdd4b60f0b7bf
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d3f060740e1c4baab87c4307c7cc019
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4e99b606795f943c1a4d8bbe8d7fef51
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 913322bbd06594c5a8f72636b1a06060
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,140 @@
fileFormatVersion: 2
guid: 37811d3829b0bfc42bfcd782271d279f
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 1
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 2cbba4ddd142149b0a38697070990deb
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 1
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: fd59b44bd3514406197f8ceb53547969
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 1
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: f6a533d9225cd40ea9ded31d4f686e3b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 1
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 0ed312066ea6f40f6af965f21c818b34
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 1
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 5eb29ddb696b54505b296d1c7a049150
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: -1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 1
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 1
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 528a6dd601766934abb8b1053bd798ef
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: -1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 1
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: 16f8cd23bf0d0480bb8ecc39be853cda
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Yarn.Unity.Editor
{
[CustomEditor(typeof(InMemoryVariableStorage))]
public class InMemoryVariableStorageEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
var varStorage = (InMemoryVariableStorage)target;
varStorage.showDebug = EditorGUILayout.Foldout(varStorage.showDebug, "Debug Variables");
if (!varStorage.showDebug)
{
return;
}
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("Not in Play Mode, so no variables to display!", MessageType.Info);
return;
}
var style = EditorStyles.label;
var list = varStorage.GetDebugList();
var height = style.CalcHeight(new GUIContent(list), EditorGUIUtility.currentViewWidth);
EditorGUILayout.SelectableLabel(list, GUILayout.MaxHeight(height), GUILayout.ExpandHeight(true));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 230a34f0c8ffc4b45b5f098c65d69492
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,145 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace Yarn.Unity.Editor
{
public class LanguageField : BaseField<string>
{
PopupField<string?> m_Popup;
TextField m_TextField;
Dictionary<string, Culture> KnownCultures = new Dictionary<string, Culture>();
CultureInfo? DefaultCulture;
public LanguageField() : this(null, false) { }
public LanguageField(string? label, bool onlyNeutralCultures = false) : base(label, new Label() { })
{
var container = new VisualElement();
container.style.flexDirection = FlexDirection.Column;
container.style.flexGrow = 1;
Add(container);
// This is the input element instantiated for the base constructor.
KnownCultures = Cultures.GetCultures().Where(c => !onlyNeutralCultures || c.IsNeutralCulture).ToDictionary(kv => kv.Name);
DefaultCulture = System.Globalization.CultureInfo.CurrentCulture;
if (onlyNeutralCultures && DefaultCulture.IsNeutralCulture == false)
{
DefaultCulture = DefaultCulture.Parent;
}
var cultureChoices = KnownCultures.Keys.Prepend(null).ToList();
static string FormatCulture(string? cultureName)
{
if (cultureName == null)
{
return "Custom";
}
else
{
return Cultures.TryGetCulture(cultureName, out var culture)
? $"{culture.DisplayName} ({culture.Name})"
: cultureName;
}
}
m_Popup = new PopupField<string?>(null, cultureChoices, DefaultCulture.Name, FormatCulture, FormatCulture);
m_Popup.style.flexGrow = 1;
m_Popup.RegisterValueChangedCallback(OnPopupValueChanged);
container.Add(m_Popup);
m_TextField = new TextField();
m_TextField.style.flexGrow = 1;
m_TextField.RegisterValueChangedCallback(OnTextFieldValueChanged);
container.Add(m_TextField);
m_Popup.style.marginRight = 0;
m_TextField.style.marginRight = 0;
this.style.flexGrow = 1;
//m_Input.RegisterValueChangedCallback(OnValueChanged);
//m_Input2.RegisterValueChangedCallback(OnValueChanged);
}
public override string value
{
get => base.value; set
{
Debug.Log($"Language value changed from {base.value} to {value}");
base.value = value;
}
}
void OnTextFieldValueChanged(ChangeEvent<string> evt)
{
this.value = evt.newValue;
}
void OnPopupValueChanged(ChangeEvent<string?> evt)
{
if (evt.newValue != null)
{
this.value = evt.newValue;
m_TextField.style.display = DisplayStyle.None;
}
else
{
// The popup has changed to the 'custom' value; show the text field
m_TextField.style.display = DisplayStyle.Flex;
}
}
public override void SetValueWithoutNotify(string newValue)
{
m_TextField.value = newValue;
if (m_TextField.focusController?.focusedElement == m_TextField)
{
// The text field has focus; do not change its visibility
}
else
{
m_TextField.style.display = (!KnownCultures.ContainsKey(newValue) || m_Popup.value == null) ? DisplayStyle.Flex : DisplayStyle.None;
}
if (KnownCultures.ContainsKey(newValue))
{
m_Popup.SetValueWithoutNotify(newValue);
}
else
{
m_Popup.SetValueWithoutNotify(null);
}
base.SetValueWithoutNotify(newValue);
}
}
public static class LanguagePopup
{
// TODO: Remove, not needed in Unity 2022
public static T ReplaceElement<T>(VisualElement oldElement, T newElement) where T : VisualElement
{
oldElement.parent.Insert(oldElement.parent.IndexOf(oldElement) + 1, newElement);
oldElement.RemoveFromHierarchy();
return newElement;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: afcc06473aee040aa87e08d80c09c981
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,68 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Yarn.Unity.Attributes;
namespace Yarn.Unity.Editor
{
[CustomPropertyDrawer(typeof(LanguageAttribute))]
public class LanguageAttributeEditor : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
using (var scope = new EditorGUI.PropertyScope(position, label, property))
{
// If this property is not a string, show an error label. (We
// can't call EditorGUI.PropertyField, because that would cause
// an infinite recursion - Unity would invoke this property
// drawer again.)
if (property.propertyType != SerializedPropertyType.String)
{
EditorGUI.HelpBox(position, $"{property.name} is not a string.", MessageType.Error);
return;
}
// Display this property as a dropdown that lets you select a
// language.
var allCultures = Cultures.GetCultures().ToList();
var indices = Enumerable.Range(0, allCultures.Count());
var culturesToIndicies = allCultures.Zip(indices, (culture, index) => new { culture, index }).ToDictionary(pair => pair.culture.Name, pair => pair.index);
var value = property.stringValue;
int currentCultureIndex;
if (culturesToIndicies.ContainsKey(value))
{
currentCultureIndex = culturesToIndicies[value];
}
else
{
// The property doesn't contain a valid culture name. Show
// an 'empty' value, which will be replaced when the user
// selects a valid value from the dropdown.
currentCultureIndex = -1;
}
var allCultureDisplayNames = allCultures.Select(c => (c.DisplayName + $":({c.Name})")).Select(n => new GUIContent(n)).ToArray();
using (var changeCheck = new EditorGUI.ChangeCheckScope())
{
var selectedIndex = EditorGUI.Popup(position, label, currentCultureIndex, allCultureDisplayNames);
if (changeCheck.changed)
{
property.stringValue = allCultures[selectedIndex].Name;
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 321b50cd267214a08ba7310de9a39764
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,472 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Yarn.Unity;
#if USE_ADDRESSABLES
using UnityEngine.AddressableAssets;
using UnityEditor.AddressableAssets;
#endif
#nullable enable
namespace Yarn.Unity.Editor
{
internal class ImportLocalizationFromAssetWindow : EditorWindow
{
private System.Type? assetType;
public LocalizationEditor? Target { get; private set; }
public string FieldLabel { get; set; } = "Source Asset";
public string? HelpBox { get; set; }
public static ImportLocalizationFromAssetWindow Show<T>(LocalizationEditor target, string windowTitle, System.Action<T> onImport) where T : UnityEngine.Object
{
var window = EditorWindow.GetWindow<ImportLocalizationFromAssetWindow>(true, windowTitle);
window.Target = target;
window.assetType = typeof(T);
window.maxSize = new Vector2(300, 200);
window.onImport = (Object obj) => onImport((T)obj);
window.ShowUtility();
return window;
}
UnityEngine.Object? asset = null;
private System.Action<UnityEngine.Object>? onImport;
public void OnGUI()
{
if (Target == null)
{
// Our target went away; close this window
this.Close();
}
asset = EditorGUILayout.ObjectField(FieldLabel, asset, assetType, allowSceneObjects: false);
if (string.IsNullOrEmpty(this.HelpBox) == false)
{
EditorGUILayout.HelpBox(this.HelpBox, MessageType.Info);
}
GUILayout.FlexibleSpace();
using (new EditorGUI.DisabledScope(asset == null))
{
if (GUILayout.Button("Import") && asset != null)
{
onImport?.Invoke(asset);
this.Close();
}
}
}
}
[CustomEditor(typeof(Localization))]
[CanEditMultipleObjects]
public class LocalizationEditor : UnityEditor.Editor
{
private SerializedProperty? entriesProperty;
private SerializedProperty? usesUnityAddressablesProperty;
private AudioClip? lastPreviewed;
private List<Culture>? cultures;
private int currentPickerWindow;
private void OnEnable()
{
entriesProperty = serializedObject.FindProperty(nameof(Localization.entries));
usesUnityAddressablesProperty = serializedObject.FindProperty(nameof(Localization._usesAddressableAssets));
lastPreviewed = null;
cultures = Cultures.GetCultures().ToList();
}
public override void OnInspectorGUI()
{
var target = this.target as Localization;
if (target == null)
{
throw new System.InvalidOperationException($"Target is not a {typeof(Localization)}");
}
var isSubAsset = AssetDatabase.IsSubAsset(target);
if (serializedObject.isEditingMultipleObjects)
{
EditorGUILayout.HelpBox($"Select a single {nameof(Localization).ToLowerInvariant()} to view its contents.", MessageType.None);
}
else
{
if (isSubAsset)
{
DrawLocalizationContentsPreview(target);
}
else
{
#if USE_ADDRESSABLES
EditorGUILayout.PropertyField(usesUnityAddressablesProperty);
EditorGUILayout.Space();
#else
if (usesUnityAddressablesProperty != null && usesUnityAddressablesProperty.boolValue)
{
EditorGUILayout.HelpBox("This Localization uses Unity Addressables, but the package is not installed.", MessageType.Warning);
EditorGUILayout.Space();
}
#endif
if (GUILayout.Button("Import String from Yarn Project"))
{
var window = ImportLocalizationFromAssetWindow.Show<YarnProject>(this, "Import from Yarn Project", ImportFromYarnProject);
window.FieldLabel = "Yarn Project";
window.HelpBox = $"The lines in the base localisation of the selected Yarn Project will be imported into this {nameof(Localization)}.";
}
if (GUILayout.Button("Import Strings from CSV"))
{
var window = ImportLocalizationFromAssetWindow.Show<TextAsset>(this, "Import from Yarn Project", ImportFromCSV);
window.FieldLabel = "CSV File";
window.HelpBox = $"The string table entries from the selected CSV file will be imported into this {nameof(Localization)}.\n\nYou can generate a CSV file to use by selecting the Yarn Project and clicking {YarnProjectImporterEditor.AddStringTagsButtonLabel}. You can then translate the CSV file into your target language, and then import it using this window.";
}
if (GUILayout.Button("Import Assets from Folder"))
{
var window = ImportLocalizationFromAssetWindow.Show<DefaultAsset>(this, "Import from Yarn Project", (folder) =>
{
var lineIDs = target.entries.Keys;
var paths = YarnProjectUtility.FindAssetPathsForLineIDs(lineIDs, AssetDatabase.GetAssetPath(folder), typeof(UnityEngine.Object));
foreach (var path in paths)
{
var lineID = path.Key;
var assetPath = path.Value;
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
target.AddLocalizedObjectToAsset<UnityEngine.Object>(lineID, asset);
#if USE_ADDRESSABLES
if (target.UsesAddressableAssets)
{
// If we're using addressable assets, make
// sure that the asset we just added has an
// address
EnsureAssetIsAddressable(asset, Localization.GetAddressForLine(lineID, target.name));
}
#endif
}
serializedObject.Update();
EditorUtility.SetDirty(target);
AssetDatabase.SaveAssetIfDirty(target);
});
window.FieldLabel = "Folder";
}
EditorGUILayout.PropertyField(entriesProperty);
}
}
if (serializedObject.hasModifiedProperties)
{
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Displays the contents of <paramref name="target"/> as a table.
/// </summary>
/// <param name="target">The <see cref="Localization"/> to show the
/// contents of.</param>
/// <param name="showAssets">If true, this method will show any
/// assets or addressable assets. If false, this method will only
/// show the localized text.</param>
private void DrawLocalizationContentsPreview(Localization target)
{
var lineKeys = target.GetLineIDs();
// Early out if we don't have any lines
if (lineKeys.Count() == 0)
{
EditorGUILayout.HelpBox($"This {nameof(Localization).ToLowerInvariant()} does not contain any lines.", MessageType.Info);
return;
}
var localizedLineContent = new List<(string ID, string Line, Object? Asset)>();
var anyAssetsFound = false;
foreach (var key in lineKeys)
{
if (target.entries.TryGetValue(key, out var entry) == false)
{
// We somehow don't have a value for this line ID?
Debug.LogError($"Internal error: failed to find an entry for {key}");
EditorGUILayout.HelpBox($"Internal error: failed to find an entry for {key}", MessageType.Error);
return;
}
string? text = target.GetLocalizedString(key);
Object? asset = null;
if (target.UsesAddressableAssets)
{
#if USE_ADDRESSABLES
asset = entry.localizedAssetReference?.editorAsset;
#endif
}
else
{
asset = entry.localizedAsset;
}
anyAssetsFound |= asset != null;
localizedLineContent.Add((key, text ?? string.Empty, asset));
}
foreach (var entry in localizedLineContent)
{
var idContent = new GUIContent(entry.ID);
// Create a GUIContent that contains the string as its text
// and also as its tooltip. This allows the user to mouse
// over this line in the inspector and see more of it.
var lineContent = new GUIContent(entry.Line, entry.Line);
// Show the line ID and localized text
EditorGUILayout.LabelField(idContent, lineContent);
if (entry.Asset != null)
{
// Asset references are never editable here - they're
// only updated by the Localization Database. Add a
// DisabledGroup here to make all ObjectFields be
// interactable, but read-only.
EditorGUI.BeginDisabledGroup(true);
// Show the object field
EditorGUILayout.ObjectField(" ", entry.Asset, typeof(UnityEngine.Object), false);
// for AudioClips, add a little play preview button
if (entry.Asset.GetType() == typeof(UnityEngine.AudioClip))
{
var rect = GUILayoutUtility.GetLastRect();
// Localization assets are displayed in an
// Inspector that's always disabled, so we need to
// manually set the enabled flag to 'true' in order
// to let this button be clickable. We'll restore
// it after we handle this button.
var wasEnabled = GUI.enabled;
GUI.enabled = true;
bool isPlaying = IsClipPlaying((AudioClip)entry.Asset);
if (lastPreviewed == (AudioClip)entry.Asset && isPlaying)
{
rect.width = 54;
rect.x += EditorGUIUtility.labelWidth - 56;
if (GUI.Button(rect, "▣ Stop"))
{
StopAllClips();
lastPreviewed = null;
}
}
else
{
rect.width = 18;
rect.x += EditorGUIUtility.labelWidth - 20;
if (GUI.Button(rect, "▸"))
{
PlayClip((AudioClip)entry.Asset);
lastPreviewed = (AudioClip)entry.Asset;
}
}
// Restore the enabled state
GUI.enabled = wasEnabled;
}
EditorGUILayout.Space();
}
else if (anyAssetsFound)
{
// Other entries have assets, but not this one. TODO:
// show a warning? probably need to make it really
// prominent, and possibly allow filtering this view to
// show only lines that have no assets?
}
}
}
// below is some terrible reflection needed for the AudioClip
// preview terrible hack from
// https://forum.unity.com/threads/way-to-play-audio-in-editor-using-an-editor-script.132042/#post-4767824
public static void PlayClip(AudioClip clip, int startSample = 0, bool loop = false)
{
System.Reflection.Assembly unityEditorAssembly = typeof(AudioImporter).Assembly;
System.Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
// The name of the method we want to invoke changed in 2020.2,
// so we'll do a little version testing here
string methodName;
methodName = "PlayPreviewClip";
System.Reflection.MethodInfo method = audioUtilClass.GetMethod(
methodName,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
null,
new System.Type[] { typeof(AudioClip), typeof(int), typeof(bool) },
null
);
method.Invoke(
null,
new object[] { clip, startSample, loop }
);
}
public static void StopAllClips()
{
System.Reflection.Assembly unityEditorAssembly = typeof(AudioImporter).Assembly;
System.Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
// The name of the method we want to invoke changed in 2020.2,
// so we'll do a little version testing here
string methodName = "StopAllPreviewClips";
System.Reflection.MethodInfo method = audioUtilClass.GetMethod(
methodName,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
null,
new System.Type[] { },
null
);
method.Invoke(
null,
new object[] { }
);
}
public static bool IsClipPlaying(AudioClip clip)
{
System.Reflection.Assembly unityEditorAssembly = typeof(AudioImporter).Assembly;
System.Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
System.Reflection.MethodInfo method = audioUtilClass.GetMethod(
"IsPreviewClipPlaying",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public
);
return (bool)method.Invoke(
null,
null
);
}
internal void ImportFromYarnProject(YarnProject project)
{
var target = this.target as Localization;
if (target == null)
{
return;
}
var lineIDs = project.baseLocalization.GetLineIDs();
target.UsesAddressableAssets = project.baseLocalization.UsesAddressableAssets;
foreach (var (id, entry) in project.baseLocalization.entries)
{
var localizedString = entry.localizedString;
if (localizedString != null)
{
target.AddLocalisedStringToAsset(id, localizedString);
}
Object? asset = null;
if (project.baseLocalization.UsesAddressableAssets)
{
#if USE_ADDRESSABLES
asset = entry.localizedAssetReference?.editorAsset;
#endif
}
else if (entry.localizedAsset != null)
{
asset = entry.localizedAsset;
}
if (asset != null)
{
target.AddLocalizedObjectToAsset(id, asset);
}
}
serializedObject.Update();
EditorUtility.SetDirty(target);
AssetDatabase.SaveAssetIfDirty(target);
}
internal void ImportFromCSV(TextAsset asset)
{
var target = this.target as Localization;
if (target == null)
{
return;
}
try
{
var stringTable = StringTableEntry.ParseFromCSV(asset.text);
foreach (var entry in stringTable)
{
target.AddLocalisedStringToAsset(entry.ID, entry.Text ?? string.Empty);
}
serializedObject.Update();
EditorUtility.SetDirty(target);
AssetDatabase.SaveAssetIfDirty(target);
}
catch (System.ArgumentException e)
{
Debug.LogWarning($"Failed to import localization from CSV because an error was encountered during text parsing: {e}");
}
}
#if USE_ADDRESSABLES
internal static void EnsureAssetIsAddressable(Object asset, string defaultAddress)
{
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out string guid, out long _) == false)
{
Debug.LogError($"Can't make {asset} addressable: no GUID found", asset);
return;
}
// Find the existing entry for this asset, if it has one.
UnityEditor.AddressableAssets.Settings.AddressableAssetEntry entry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry(guid);
if (entry != null)
{
// The asset already has an entry. Nothing to do.
return;
}
// This asset didn't have an entry. Create one in the default group.
Debug.Log($"Marking asset {AssetDatabase.GetAssetPath(asset)} as addressable", asset);
entry = AddressableAssetSettingsDefaultObject.Settings.CreateOrMoveEntry(guid, AddressableAssetSettingsDefaultObject.Settings.DefaultGroup);
// Update the entry's address.
entry.SetAddress(defaultAddress);
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d221cff8c71dc4726a348d8b718becaa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,170 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace Yarn.Unity.Editor
{
public class LocalizationEntryElement : VisualElement, INotifyValueChanged<ProjectImportData.LocalizationEntry>
{
private readonly Foldout foldout;
private readonly ObjectField assetFolderField;
private readonly ObjectField stringsFileField;
private readonly Button deleteButton;
private readonly VisualElement stringsFileNotUsedLabel;
private readonly LanguageField languagePopup;
private readonly Toggle isExternalAssetToggle;
private readonly ObjectField externalLocalisationAssetField;
private readonly VisualElement externalReferenceFields;
private readonly VisualElement internallyGeneratedAssetFields;
public event System.Action? OnDelete;
ProjectImportData.LocalizationEntry data;
public bool IsModified { get; private set; }
private string _projectBaseLanguage;
public string ProjectBaseLanguage
{
get => _projectBaseLanguage;
set
{
_projectBaseLanguage = value;
SetValueWithoutNotify(this.data);
}
}
public LocalizationEntryElement(VisualTreeAsset asset, ProjectImportData.LocalizationEntry data, string baseLanguage)
{
asset.CloneTree(this);
foldout = this.Q<Foldout>("foldout");
assetFolderField = this.Q<ObjectField>("assetFolder");
stringsFileField = this.Q<ObjectField>("stringsFile");
deleteButton = this.Q<Button>("deleteButton");
stringsFileNotUsedLabel = this.Q("stringsFileNotUsed");
isExternalAssetToggle = this.Q<Toggle>("isExternalAsset");
externalLocalisationAssetField = this.Q<ObjectField>("externalLocalisationAsset");
externalReferenceFields = this.Q<VisualElement>("externalReferenceFields");
internallyGeneratedAssetFields = this.Q<VisualElement>("internallyGeneratedAssetFields");
assetFolderField.objectType = typeof(DefaultAsset);
stringsFileField.objectType = typeof(TextAsset);
IsModified = false;
// Dropdowns don't exist in Unity 2019/20(?), so we need to create
// one at runtime and swap out a placeholder.
var existingPopup = this.Q("languagePlaceholder");
languagePopup = new LanguageField("Language");
LanguagePopup.ReplaceElement(existingPopup, languagePopup);
_projectBaseLanguage = baseLanguage;
languagePopup.RegisterValueChangedCallback((evt) =>
{
IsModified = true;
var newEntry = this.value;
newEntry.languageID = evt.newValue;
this.value = newEntry;
});
isExternalAssetToggle.RegisterValueChangedCallback((evt) =>
{
IsModified = true;
var newEntry = this.value;
newEntry.isExternal = evt.newValue;
this.value = newEntry;
});
externalLocalisationAssetField.RegisterValueChangedCallback((evt) =>
{
IsModified = true;
var newEntry = this.value;
newEntry.externalLocalization = evt.newValue as Localization;
this.value = newEntry;
});
stringsFileField.RegisterValueChangedCallback((evt) =>
{
IsModified = true;
var newEntry = this.value;
newEntry.stringsFile = evt.newValue as TextAsset;
this.value = newEntry;
});
assetFolderField.RegisterValueChangedCallback((evt) =>
{
IsModified = true;
var newEntry = this.value;
newEntry.assetsFolder = evt.newValue as DefaultAsset;
this.value = newEntry;
});
deleteButton.clicked += () => OnDelete?.Invoke();
SetValueWithoutNotify(data);
}
public ProjectImportData.LocalizationEntry value
{
get => data;
set
{
var previous = data;
SetValueWithoutNotify(value);
using (var evt = ChangeEvent<ProjectImportData.LocalizationEntry>.GetPooled(previous, value))
{
evt.target = this;
SendEvent(evt);
}
}
}
public void SetValueWithoutNotify(ProjectImportData.LocalizationEntry data)
{
this.data = data;
Culture culture;
var foundCulture = Cultures.TryGetCulture(data.languageID, out culture);
string foldoutDisplayName = foundCulture ? $"{culture.DisplayName} ({culture.Name})" : $"{data.languageID}";
languagePopup.SetValueWithoutNotify(data.languageID);
assetFolderField.SetValueWithoutNotify(data.assetsFolder);
stringsFileField.SetValueWithoutNotify(data.stringsFile);
isExternalAssetToggle.SetValueWithoutNotify(data.isExternal);
externalLocalisationAssetField.SetValueWithoutNotify(data.externalLocalization);
bool isBaseLanguage = data.languageID == ProjectBaseLanguage;
if (isBaseLanguage)
{
stringsFileField.style.display = DisplayStyle.None;
stringsFileNotUsedLabel.style.display = DisplayStyle.Flex;
}
else
{
stringsFileField.style.display = DisplayStyle.Flex;
stringsFileNotUsedLabel.style.display = DisplayStyle.None;
}
internallyGeneratedAssetFields.style.display = data.isExternal ? DisplayStyle.None : DisplayStyle.Flex;
externalReferenceFields.style.display = data.isExternal ? DisplayStyle.Flex : DisplayStyle.None;
deleteButton.SetEnabled(isBaseLanguage == false);
foldout.text = foldoutDisplayName;
}
internal void ClearModified()
{
IsModified = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0bb769319d8a44820b74cf1d9b6b87ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1031ba4c510cc4286a629d303ef48d29
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
#nullable enable
namespace Yarn.Unity.Editor
{
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
[CustomPropertyDrawer(typeof(MarkupPalette.BasicMarker))]
public class BasicMarkerPropertyDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var container = new VisualElement();
var name = property.FindPropertyRelative(nameof(MarkupPalette.BasicMarker.Marker));
var nameField = new PropertyField(name);
var showColourField = new PropertyField(property.FindPropertyRelative(nameof(MarkupPalette.BasicMarker.CustomColor)));
var colourField = new PropertyField(property.FindPropertyRelative(nameof(MarkupPalette.BasicMarker.Color)));
var boldField = new PropertyField(property.FindPropertyRelative(nameof(MarkupPalette.BasicMarker.Boldened)));
var italicsField = new PropertyField(property.FindPropertyRelative(nameof(MarkupPalette.BasicMarker.Italicised)));
var underlinedField = new PropertyField(property.FindPropertyRelative(nameof(MarkupPalette.BasicMarker.Underlined)));
var strikedField = new PropertyField(property.FindPropertyRelative(nameof(MarkupPalette.BasicMarker.Strikedthrough)));
var foldout = new Foldout { text = name.stringValue };
showColourField.RegisterCallback((ChangeEvent<bool> b) =>
{
colourField.style.display = b.newValue ? DisplayStyle.Flex : DisplayStyle.None;
});
foldout.Add(nameField);
foldout.Add(showColourField);
foldout.Add(colourField);
foldout.Add(boldField);
foldout.Add(italicsField);
foldout.Add(underlinedField);
foldout.Add(strikedField);
container.Add(foldout);
return container;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5eaeba2d10a544bf58c014880b8b71dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,238 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Linq;
using UnityEditor;
using UnityEngine;
#nullable enable
namespace Yarn.Unity.Editor
{
[CustomPropertyDrawer(typeof(YarnProjectImporter.SerializedDeclaration))]
public class DeclarationPropertyDrawer : PropertyDrawer
{
/// <summary>
/// Draws either a property field or a label field for <paramref
/// name="property"/> at <paramref name="position"/>, depending on
/// the value of <paramref name="readOnly"/>.
/// </summary>
/// <param name="position">The rectangle in which to draw the
/// control.</param>
/// <param name="property">The property to draw a control
/// for.</param>
/// <param name="readOnly">Whether the property is read-only or
/// not.</param>
private void DrawPropertyField(Rect position, SerializedProperty property, bool readOnly, string? label = null)
{
if (label == null)
{
label = property.displayName;
}
if (readOnly)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
EditorGUI.LabelField(position, label, property.intValue.ToString());
break;
case SerializedPropertyType.Boolean:
var boolText = property.boolValue ? "True" : "False";
EditorGUI.LabelField(position, label, boolText);
break;
case SerializedPropertyType.Float:
EditorGUI.LabelField(position, label, property.floatValue.ToString());
break;
case SerializedPropertyType.String:
EditorGUI.LabelField(position, label, property.stringValue);
break;
case SerializedPropertyType.ObjectReference:
using (new EditorGUI.DisabledGroupScope(true))
{
EditorGUI.ObjectField(position, property);
}
break;
case SerializedPropertyType.Enum:
var displayValue = property.enumDisplayNames[property.enumValueIndex];
EditorGUI.LabelField(position, label, displayValue);
break;
}
}
else
{
switch (property.propertyType)
{
case SerializedPropertyType.String:
property.stringValue = EditorGUI.TextField(position, label, property.stringValue);
break;
case SerializedPropertyType.Float:
property.floatValue = EditorGUI.FloatField(position, label, property.floatValue);
break;
case SerializedPropertyType.Integer:
property.floatValue = EditorGUI.IntField(position, label, property.intValue);
break;
default:
// Just use a regular field for other kinds of
// properties
EditorGUI.PropertyField(position, property, new GUIContent(label));
break;
}
}
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
// A serialized declaration is read-only if it came from a Yarn
// script. We don't allow editing those in this panel, because
// the text of the Yarn script belongs to the user.
bool propertyIsReadOnly = property.FindPropertyRelative("sourceYarnAsset").objectReferenceValue != null;
propertyIsReadOnly |= property.FindPropertyRelative("isImplicit").boolValue;
const float leftInset = 8;
Rect RectForFieldIndex(int index, int lineCount = 1)
{
float verticalOffset = EditorGUIUtility.singleLineHeight * index + EditorGUIUtility.standardVerticalSpacing * index;
float height = EditorGUIUtility.singleLineHeight * lineCount + EditorGUIUtility.standardVerticalSpacing * (lineCount - 1);
return new Rect(
position.x + leftInset,
position.y + verticalOffset,
position.width - leftInset,
height
);
}
var foldoutPosition = RectForFieldIndex(0);
SerializedProperty nameProperty = property.FindPropertyRelative("name");
string name = nameProperty.stringValue;
if (string.IsNullOrEmpty(name))
{
name = "Variable";
}
property.isExpanded = EditorGUI.Foldout(foldoutPosition, property.isExpanded, name);
if (property.isExpanded)
{
var namePosition = RectForFieldIndex(1);
var typePosition = RectForFieldIndex(2);
var defaultValuePosition = RectForFieldIndex(3);
var descriptionPosition = RectForFieldIndex(4, 2);
var sourcePosition = RectForFieldIndex(6);
DrawPropertyField(namePosition, nameProperty, propertyIsReadOnly);
SerializedProperty typeProperty = property.FindPropertyRelative("typeName");
if (propertyIsReadOnly)
{
DrawPropertyField(typePosition, typeProperty, true);
}
else
{
var popupElements = YarnProjectImporter.SerializedDeclaration.BuiltInTypesList;
var popupElementNames = popupElements.Select(t => t.Name).ToList();
var selectedIndex = popupElementNames.IndexOf(typeProperty.stringValue);
var prefixPosition = EditorGUI.PrefixLabel(typePosition, new GUIContent("Type"));
selectedIndex = EditorGUI.Popup(prefixPosition, selectedIndex, popupElementNames.ToArray());
if (selectedIndex >= 0 && selectedIndex <= popupElementNames.Count)
{
typeProperty.stringValue = popupElementNames[selectedIndex];
}
}
SerializedProperty? defaultValueProperty;
var type = YarnProjectImporter.SerializedDeclaration.BuiltInTypesList.FirstOrDefault(t => t.Name == typeProperty.stringValue);
if (type == Types.Number)
{
defaultValueProperty = property.FindPropertyRelative("defaultValueNumber");
}
else if (type == Types.String)
{
defaultValueProperty = property.FindPropertyRelative("defaultValueString");
}
else if (type == Types.Boolean)
{
defaultValueProperty = property.FindPropertyRelative("defaultValueBool");
}
else
{
defaultValueProperty = null;
}
if (defaultValueProperty == null)
{
EditorGUI.LabelField(defaultValuePosition, "Default Value", $"Variable type {typeProperty.stringValue} is not allowed");
}
else
{
DrawPropertyField(defaultValuePosition, defaultValueProperty, propertyIsReadOnly, "Default Value");
}
// Don't use DrawPropertyField here because we want to use
// a special gui style and directly use the string value
SerializedProperty descriptionProperty = property.FindPropertyRelative("description");
if (propertyIsReadOnly)
{
descriptionPosition = EditorGUI.PrefixLabel(descriptionPosition, new GUIContent(descriptionProperty.displayName));
EditorGUI.SelectableLabel(descriptionPosition, descriptionProperty.stringValue, EditorStyles.wordWrappedLabel);
}
else
{
var wordWrappedTextField = EditorStyles.textField;
wordWrappedTextField.wordWrap = true;
descriptionProperty.stringValue = EditorGUI.TextField(descriptionPosition, descriptionProperty.displayName, descriptionProperty.stringValue, wordWrappedTextField);
}
if (!propertyIsReadOnly)
{
EditorGUI.LabelField(sourcePosition, "Declared In", "this file");
}
else
{
SerializedProperty sourceProperty = property.FindPropertyRelative("sourceYarnAsset");
EditorGUI.ObjectField(sourcePosition, "Declared In", sourceProperty.objectReferenceValue, typeof(TextAsset), false);
}
}
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return GetPropertyHeightImpl(property, label);
}
public static float GetPropertyHeightImpl(SerializedProperty property, GUIContent label)
{
int lines;
if (property.isExpanded)
{
lines = 7;
}
else
{
lines = 1;
}
return EditorGUIUtility.singleLineHeight * lines + EditorGUIUtility.standardVerticalSpacing * lines + 1;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 05c509d58b0f0441995b7356c9876e60
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,82 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using UnityEditor;
using UnityEngine;
namespace Yarn.Unity.Editor
{
[CustomPropertyDrawer(typeof(FunctionInfo))]
public class DerivedFunctionsPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
const float leftInset = 8;
Rect RectForFieldIndex(int index, int lineCount = 1)
{
float verticalOffset = EditorGUIUtility.singleLineHeight * index + EditorGUIUtility.standardVerticalSpacing * index;
float height = EditorGUIUtility.singleLineHeight * lineCount + EditorGUIUtility.standardVerticalSpacing * (lineCount - 1);
return new Rect(
position.x + leftInset,
position.y + verticalOffset,
position.width - leftInset,
height
);
}
var foldoutPosition = RectForFieldIndex(0);
SerializedProperty nameProperty = property.FindPropertyRelative("Name");
string name = nameProperty?.stringValue ?? "FUNCTION NAME";
property.isExpanded = EditorGUI.Foldout(foldoutPosition, property.isExpanded, name);
if (property.isExpanded)
{
EditorGUI.indentLevel += 1;
var typePosition = RectForFieldIndex(1);
var paramPosition = RectForFieldIndex(2);
SerializedProperty typeProperty = property.FindPropertyRelative("ReturnType");
EditorGUI.LabelField(typePosition, typeProperty?.stringValue ?? "RETURN");
SerializedProperty paramProperty = property.FindPropertyRelative("Parameters");
int count = paramProperty?.arraySize ?? 0;
if (count > 0)
{
string[] p = new string[count];
for (int i = 0; i < count; i++)
{
p[i] = paramProperty.GetArrayElementAtIndex(i).stringValue;
}
EditorGUI.LabelField(paramPosition, $"({string.Join(", ", p)})");
}
else
{
EditorGUI.LabelField(paramPosition, $"No Parameters");
}
EditorGUI.indentLevel -= 1;
}
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
int lines = 1;
if (property.isExpanded)
{
lines = 3;
}
return EditorGUIUtility.singleLineHeight * lines + EditorGUIUtility.standardVerticalSpacing * lines + 1;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ff5754fb8fe74e3fa7e65606e3feefa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using UnityEditor;
using UnityEngine;
namespace Yarn.Unity
{
[CustomPropertyDrawer(typeof(DialogueReference))]
public class DialogueReferenceDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
position = EditorGUI.PrefixLabel(position, label);
var projectProperty = property.FindPropertyRelative(nameof(DialogueReference.project));
var nodeNameProperty = property.FindPropertyRelative(nameof(DialogueReference.nodeName));
position = GetLineRect(position, out var projectRect);
position = GetLineRect(position, out var nodeNameRect);
EditorGUI.PropertyField(projectRect, projectProperty, GUIContent.none);
EditorGUI.PropertyField(nodeNameRect, nodeNameProperty, GUIContent.none);
}
/// <summary>
/// Given a rectangle, computes a rectangle of the same width as the
/// input with the height of a single line, taking into account vertical
/// spacing.
/// </summary>
/// <param name="input">A rectangle representing the total area
/// availalbe.</param>
/// <param name="lineRect">On return, a rectangle with the same width as
/// <paramref name="input"/> and with a single line's worth of
/// height.</param>
/// <returns>The remaining available space.</returns>
private Rect GetLineRect(Rect input, out Rect lineRect)
{
lineRect = input;
lineRect.height = EditorGUIUtility.singleLineHeight;
var remainder = input;
var offset = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
remainder.y += offset;
remainder.height -= offset;
return remainder;
}
/// <summary>
/// Returns the height, in pixels, needed for drawing the inspector for
/// a <see cref="DialogueReference"/>.
/// </summary>
/// <param name="property">A serialized property representing a <see
/// cref="DialogueReference"/> object.</param>
/// <param name="label">The label to display for <paramref
/// name="property"/>.</param>
/// <returns>The displayed height for the property.</returns>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
const int lineCount = 2;
return lineCount * EditorGUIUtility.singleLineHeight +
(lineCount - 1) * EditorGUIUtility.standardVerticalSpacing;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2e67f1d1bf1140a6910d4cc0225235c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,303 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Yarn.Unity.Attributes;
#nullable enable
namespace Yarn.Unity
{
/// <summary>
/// Property drawer for <see cref="DialogueReference"/>
/// </summary>
[CustomPropertyDrawer(typeof(YarnNodeAttribute))]
public class YarnNodeAttributeDrawer : PropertyDrawer
{
private const string NodeTextControlNamePrefix = "DialogueReference.NodeName.";
private YarnProject? lastProject;
private string? lastNodeName;
private bool referenceExists;
private bool editNodeAsText;
private bool focusNodeTextField;
private GUIContent? nodenameContent;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// -- Yarn Project asset reference
SerializedProperty projectProp;
if (this.attribute is not YarnNodeAttribute attribute)
{
throw new System.InvalidOperationException($"Internal error: attribute is not a {nameof(YarnNodeAttribute)}");
}
var propertyPathComponents = new System.Collections.Generic.Stack<string>(property.propertyPath.Split('.'));
while (true)
{
string testPath;
if (propertyPathComponents.Count == 0)
{
testPath = attribute.yarnProjectAttribute;
}
else
{
var components = new System.Collections.Generic.List<string>(propertyPathComponents);
components.Reverse();
testPath = string.Join(".", components) + "." + attribute.yarnProjectAttribute;
}
projectProp = property.serializedObject.FindProperty(testPath);
if (projectProp != null)
{
break;
}
if (propertyPathComponents.Count > 0)
{
propertyPathComponents.Pop();
}
else
{
break;
}
}
if (projectProp == null)
{
EditorGUI.HelpBox(position, $"{attribute.yarnProjectAttribute} does not exist on {property.serializedObject.targetObject.name}", MessageType.Error);
return;
}
EditorGUI.BeginProperty(position, label, property);
var controlId = GUIUtility.GetControlID(FocusType.Passive);
position = EditorGUI.PrefixLabel(position, controlId, label);
var indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var nodeNameFieldPosition = position;
var project = projectProp.hasMultipleDifferentValues ? null : projectProp.objectReferenceValue as YarnProject;
// -- Node name drop down
// If we want to edit this nodes name as a text field, or if we have
// multiple values, or if we have no project and we don't need one,
// show a text field and not a dropdown.
if ((project == null && attribute.requiresYarnProject == false) || editNodeAsText || projectProp.hasMultipleDifferentValues)
{
var controlName = NodeTextControlNamePrefix + controlId;
// Multi-selection with different projects, just show a text
// field to edit the node name. Most of the time, it will show
// the mixed value dash (—).
GUI.SetNextControlName(controlName);
using (var change = new EditorGUI.ChangeCheckScope())
{
var currentText = property.hasMultipleDifferentValues ? "-" : property.stringValue;
currentText = EditorGUI.TextField(nodeNameFieldPosition, currentText);
if (change.changed)
{
property.stringValue = currentText;
property.serializedObject.ApplyModifiedProperties();
}
}
if (editNodeAsText)
{
if (focusNodeTextField)
{
// Focusing the text field is delayed like this because
// the control needs to exist first before we can focus
// it
focusNodeTextField = false;
EditorGUI.FocusTextInControl(controlName);
}
else if (ShouldEndEditing(controlName))
{
editNodeAsText = false;
HandleUtility.Repaint();
}
}
}
else
{
// Show a dropdown that lets the user choose a node from the
// ones present in the Yarn Project.
// If the Yarn Project is not set, this dropdown is empty and
// disabled.
if (project == null)
{
using (new EditorGUI.DisabledGroupScope(true))
{
EditorGUI.DropdownButton(nodeNameFieldPosition, GUIContent.none, FocusType.Passive);
}
}
else
{
var nodeName = property.stringValue;
var nodeNameSet = !string.IsNullOrEmpty(nodeName);
// Cached check if node exists in project
if (lastProject != project || lastNodeName != nodeName)
{
lastProject = project;
lastNodeName = nodeName;
referenceExists = project.Program.Nodes.ContainsKey(nodeName);
}
if (nodenameContent == null)
{
nodenameContent = new GUIContent();
}
// Show warning icon if not does not exist in selected project
if (nodeNameSet)
{
nodenameContent.text = nodeName;
}
else
{
nodenameContent.text = "(Choose Node)";
}
MessageType iconType = MessageType.None;
if (!nodeNameSet)
{
iconType = MessageType.Info;
}
else if (!referenceExists)
{
iconType = MessageType.Warning;
}
else
{
iconType = MessageType.None;
}
switch (iconType)
{
case MessageType.Info:
nodenameContent.image = EditorGUIUtility.isProSkin ? EditorGUIUtility.IconContent("d_console.infoicon.sml").image : EditorGUIUtility.IconContent("console.infoicon.sml").image;
break;
case MessageType.Warning:
nodenameContent.image = EditorGUIUtility.isProSkin ? EditorGUIUtility.IconContent("d_console.warnicon.sml").image : EditorGUIUtility.IconContent("console.warnicon.sml").image;
break;
default:
nodenameContent.image = null;
break;
}
var hasMixedNodeValues = property.hasMultipleDifferentValues;
EditorGUI.showMixedValue = hasMixedNodeValues;
// Generate menu with node list only when user actually opens it
if (EditorGUI.DropdownButton(nodeNameFieldPosition, nodenameContent, FocusType.Keyboard))
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Edit..."), false, () =>
{
editNodeAsText = true;
focusNodeTextField = true;
});
menu.AddSeparator("");
menu.AddItem(new GUIContent("<None>"), !nodeNameSet, () =>
{
property.stringValue = "";
property.serializedObject.ApplyModifiedProperties();
});
if (!referenceExists && nodeNameSet && !hasMixedNodeValues)
{
menu.AddItem(new GUIContent(nodeName + " (Missing)"), true, () =>
{
property.stringValue = nodeName;
property.serializedObject.ApplyModifiedProperties();
});
}
foreach (var node in GetNodes(project))
{
var name = node.Name;
menu.AddItem(new GUIContent(name), name == nodeName && !hasMixedNodeValues, () =>
{
property.stringValue = name;
property.serializedObject.ApplyModifiedProperties();
});
}
menu.DropDown(nodeNameFieldPosition);
}
}
}
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
private static IEnumerable<Node> GetNodes(YarnProject project)
{
foreach (var node in project.Program.Nodes.Values)
{
if (node.Name.StartsWith("$"))
{
// Skip smart variable nodes
continue;
}
bool isNodeGroup = false;
foreach (var header in node.Headers)
{
if (header.Key == Node.NodeGroupHeader)
{
// This node is part of a node group; don't include it
isNodeGroup = true;
break;
}
}
if (!isNodeGroup)
{
yield return node;
}
}
}
private static bool ShouldEndEditing(string controlName)
{
if (GUI.GetNameOfFocusedControl() != controlName)
{
return false;
}
var keyCode = Event.current.keyCode;
if (keyCode != KeyCode.Return && keyCode != KeyCode.KeypadEnter)
{
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac84e49b2dc3b4c65a64a401894969d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace Yarn.Unity.Editor
{
public class SourceFileEntryElement : VisualElement, INotifyValueChanged<string>
{
private readonly TextField sourceFileField;
private readonly Button deleteButton;
public event System.Action? OnDelete;
public bool IsModified { get; private set; }
public string path = "";
public SourceFileEntryElement(VisualTreeAsset asset, string path, YarnProjectImporter importer)
{
asset.CloneTree(this);
sourceFileField = this.Q<TextField>("sourceFile");
deleteButton = this.Q<Button>("deleteButton");
IsModified = false;
sourceFileField.RegisterValueChangedCallback((evt) =>
{
IsModified = true;
this.value = evt.newValue;
});
deleteButton.clicked += () => OnDelete?.Invoke();
SetValueWithoutNotify(path);
}
public string value
{
get => path;
set
{
var previous = path;
SetValueWithoutNotify(value);
using (var evt = ChangeEvent<string>.GetPooled(previous, value))
{
evt.target = this;
SendEvent(evt);
}
}
}
public void SetValueWithoutNotify(string data)
{
this.path = data;
sourceFileField.SetValueWithoutNotify(data);
}
public void ClearModified()
{
this.IsModified = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b2fe3167ad87422aa03ae72c9d356b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f364120dc53794c46bf0dddcc6618412
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:Foldout text="Language" name="foldout" picking-mode="Ignore">
<ui:VisualElement style="flex-direction: row; flex-shrink: 0; flex-grow: 1; justify-content: space-between;">
<ui:Label text="(Language placeholder)" display-tooltip-when-elided="true" name="languagePlaceholder" style="flex-grow: 1;" />
<ui:Button name="deleteButton" text="Delete" style="flex-shrink: 1; align-items: flex-end;" />
</ui:VisualElement>
<ui:Toggle label="External Asset" name="isExternalAsset" />
<ui:VisualElement name="externalReferenceFields" enabled="true" style="flex-grow: 1; display: flex;">
<uie:ObjectField label="Localization Asset" name="externalLocalisationAsset" type="Yarn.Unity.Localization, YarnSpinner.Unity" />
</ui:VisualElement>
<ui:VisualElement name="internallyGeneratedAssetFields" style="flex-grow: 1;">
<uie:ObjectField label="Strings File" allow-scene-objects="false" type="UnityEngine.TextAsset, UnityEngine.CoreModule" name="stringsFile" />
<ui:VisualElement name="stringsFileNotUsed" class="stringsFileNotUsed" style="flex-direction: row;">
<ui:Label text="Strings File" display-tooltip-when-elided="true" class="unity-base-field__label" style="margin-left: 3px;" />
<ui:Label text="Automatically included" display-tooltip-when-elided="true" class="unity-base-field__label" />
</ui:VisualElement>
<uie:ObjectField label="Assets Folder" allow-scene-objects="false" type="UnityEditor.DefaultAsset, UnityEditor" name="assetFolder" />
</ui:VisualElement>
<ui:VisualElement style="align-items: flex-end;" />
</ui:Foldout>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: de04e2bdef0b44420ace51d11acb9a26
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:VisualElement style="flex-direction: row;">
<ui:TextField picking-mode="Ignore" name="sourceFile" style="flex-grow: 1;" />
<ui:Button text="Delete" display-tooltip-when-elided="true" name="deleteButton" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 538a1dbae42414629a134a058c8848d9
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,20 @@
.help-box {
background-color: var(--unity-colors-helpbox-background);
border-color: var(--unity-colors-helpbox-border);
border-width: 1px;
border-radius: 2px;
padding: 8px;
margin: 8px;
}
.help-box Label {
white-space: normal;
padding-bottom: 8px;
}
.help-box Label.link {
white-space: normal;
color: var(--unity-colors-link-text);
cursor: link;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb55d9cdb86eb461d9bf7e96af6e9c1e
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

Some files were not shown because too many files have changed in this diff Show More