You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

173 lines
5.8 KiB
C#

#if TOOLS
#nullable enable
using Godot;
using Ink;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using DependenciesCache = System.Collections.Generic.Dictionary<string, string[]>;
using GArrayGictionary = Godot.Collections.Array<Godot.Collections.Dictionary>;
using GArrayString = Godot.Collections.Array<string>;
using Gictionary = Godot.Collections.Dictionary;
namespace GodotInk;
[Tool]
public partial class InkStoryImporter : EditorImportPlugin
{
private const string OPT_MAIN_FILE = "is_main_file";
private const string OPT_COMPRESS = "compress";
private static readonly Regex includeRegex = new(@"^\s*INCLUDE\s*(?<Path>.*)\s*$", RegexOptions.Multiline | RegexOptions.Compiled);
#pragma warning disable IDE0022
public override string _GetImporterName() => "ink";
public override string _GetVisibleName() => "Ink story";
public override string[] _GetRecognizedExtensions() => new string[] { "ink" };
public override string _GetResourceType() => nameof(Resource);
public override string _GetSaveExtension() => "res";
public override float _GetPriority() => 1.0f;
public override int _GetPresetCount() => 0;
public override int _GetImportOrder() => 0;
public override GArrayGictionary _GetImportOptions(string path, int presetIndex) => new()
{
new() { { "name", OPT_MAIN_FILE }, { "default_value", false } },
new() { { "name", OPT_COMPRESS }, { "default_value", true } }
};
public override bool _GetOptionVisibility(string path, StringName optionName, Gictionary options) => true;
#pragma warning restore IDE0022
public override Error _Import(string sourceFile, string savePath, Gictionary options, GArrayString _, GArrayString __)
{
UpdateCache(sourceFile, ExtractIncludes(sourceFile));
string destFile = $"{savePath}.{_GetSaveExtension()}";
Error returnValue = options[OPT_MAIN_FILE].AsBool()
? ImportFromInk(sourceFile, destFile, options[OPT_COMPRESS].AsBool())
: ResourceSaver.Save(new StubInkStory(), destFile);
string[] additionalFiles = GetCache().Where(kvp => kvp.Value.Contains(sourceFile)).Select(kvp => kvp.Key).ToArray();
foreach (var additionalFile in additionalFiles)
AppendImportExternalResource(additionalFile);
return returnValue;
}
private static Error ImportFromInk(string sourceFile, string destFile, bool shouldCompress)
{
using Godot.FileAccess file = Godot.FileAccess.Open(sourceFile, Godot.FileAccess.ModeFlags.Read);
if (file == null)
return Godot.FileAccess.GetOpenError();
Compiler compiler = new(file.GetAsText(), new Compiler.Options
{
sourceFilename = sourceFile,
errorHandler = InkCompilerErrorHandler,
fileHandler = new FileHandler(
Path.GetDirectoryName(file.GetPathAbsolute()) ?? ProjectSettings.GlobalizePath("res://")
),
});
try
{
string storyContent = compiler.Compile().ToJson();
InkStory resource = InkStory.Create(storyContent);
ResourceSaver.SaverFlags flags = shouldCompress ? ResourceSaver.SaverFlags.Compress
: ResourceSaver.SaverFlags.None;
return ResourceSaver.Save(resource, destFile, flags);
}
catch (InvalidInkException)
{
return Error.CompilationFailed;
}
}
private static List<string> ExtractIncludes(string sourceFile)
{
using Godot.FileAccess file = Godot.FileAccess.Open(sourceFile, Godot.FileAccess.ModeFlags.Read);
return includeRegex.Matches(file.GetAsText())
.OfType<Match>()
.Select(match => sourceFile.GetBaseDir().PathJoin(match.Groups["Path"].Value.TrimEnd('\r')))
.ToList();
}
private static void InkCompilerErrorHandler(string message, ErrorType errorType)
{
switch (errorType)
{
case ErrorType.Warning:
GD.PushWarning(message);
break;
case ErrorType.Error:
GD.PushError(message);
throw new InvalidInkException();
case ErrorType.Author:
default:
break;
}
}
private const string CACHE_FILE = "user://ink_cache.json";
public static void UpdateCache(string sourceFile, List<string> dependencies)
{
DependenciesCache cache = GetCache();
cache[sourceFile] = dependencies.ToArray();
cache = cache.Where(kvp => kvp.Value.Length > 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
using Godot.FileAccess file = Godot.FileAccess.Open(CACHE_FILE, Godot.FileAccess.ModeFlags.Write);
file?.StoreString(JsonSerializer.Serialize(cache));
}
public static DependenciesCache GetCache()
{
using Godot.FileAccess file = Godot.FileAccess.Open(CACHE_FILE, Godot.FileAccess.ModeFlags.Read);
try
{
return JsonSerializer.Deserialize<DependenciesCache>(file?.GetAsText() ?? "{}")
?? new DependenciesCache();
}
catch (JsonException)
{
return new DependenciesCache();
}
}
private class FileHandler : IFileHandler
{
private readonly string rootDir;
public FileHandler(string rootDir)
{
this.rootDir = rootDir;
}
public string ResolveInkFilename(string includeName)
{
return Path.Combine(rootDir, includeName);
}
public string LoadInkFileContents(string fullFilename)
{
return File.ReadAllText(fullFilename);
}
}
}
#endif