diff --git a/Las gymkhanikas de Uli.csproj b/Las gymkhanikas de Uli.csproj
new file mode 100644
index 00000000..ba5a9d9c
--- /dev/null
+++ b/Las gymkhanikas de Uli.csproj
@@ -0,0 +1,8 @@
+
+
+ net8.0
+ net9.0
+ true
+ LasgymkhanikasdeUli
+
+
\ No newline at end of file
diff --git a/Las gymkhanikas de Uli.sln b/Las gymkhanikas de Uli.sln
new file mode 100644
index 00000000..ae9cfc63
--- /dev/null
+++ b/Las gymkhanikas de Uli.sln
@@ -0,0 +1,19 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Las gymkhanikas de Uli", "Las gymkhanikas de Uli.csproj", "{9E3EA186-FBCB-4B4D-BF1C-89064EF61C6C}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ ExportDebug|Any CPU = ExportDebug|Any CPU
+ ExportRelease|Any CPU = ExportRelease|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9E3EA186-FBCB-4B4D-BF1C-89064EF61C6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9E3EA186-FBCB-4B4D-BF1C-89064EF61C6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9E3EA186-FBCB-4B4D-BF1C-89064EF61C6C}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
+ {9E3EA186-FBCB-4B4D-BF1C-89064EF61C6C}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
+ {9E3EA186-FBCB-4B4D-BF1C-89064EF61C6C}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
+ {9E3EA186-FBCB-4B4D-BF1C-89064EF61C6C}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/README.md b/README.md
index 6acbfeed..53a79428 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,13 @@ Point-and-click adventure game developed using Escoria framework and Godot engin
2) Clone `escoria-demo-game` repo (develop branch)
3) Create `gymkhana/addons/escoria-core` symlink pointing to `escoria-demo-game/addons/escoria-core`
+### Frog Subtitles
+
+Frog Subtitles is a C# plugin which requires some extra steps:
+
+1) Use Godot .Net version
+2) Download .Net SDK v8+ (Ubuntu 24.04: `sudo apt install dotnet-sdk-8.0`)
+3) Build C# solution in Godot: Project -> Tools -> C# -> Create C# solution
## Video export.
- 1280 x 720 | 25fps
diff --git a/addons/frog_subtitles/SubtitlesImportPlugin.cs b/addons/frog_subtitles/SubtitlesImportPlugin.cs
new file mode 100644
index 00000000..3e960aa5
--- /dev/null
+++ b/addons/frog_subtitles/SubtitlesImportPlugin.cs
@@ -0,0 +1,176 @@
+#if TOOLS
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using Godot;
+using Godot.Collections;
+
+namespace Frog;
+
+public partial class SubtitlesImportPlugin : EditorImportPlugin
+{
+ private readonly static Regex timeRegex = new Regex(@"(?[0-9,:\.]+)\ -->\ (?[0-9,:\.]+)");
+ private readonly static Regex colorRegex = new Regex(@"");
+ private readonly static Regex linePositionRegex = new Regex(@"\{\\a([0-9]+)\}");
+
+ public override string _GetImporterName() => "frog-subtitles.plugin";
+
+ public override string _GetVisibleName() => "Frog Subtitles";
+
+ public override string[] _GetRecognizedExtensions() => new string[] { "srt" };
+
+ public override string _GetSaveExtension() => "res";
+
+ public override string _GetResourceType() => "Resource";
+
+ public override int _GetPresetCount() => 1;
+
+ public override float _GetPriority() => 1;
+
+ public override int _GetImportOrder() => 0;
+
+ public override string _GetPresetName(int presetIndex) => "Default";
+
+ public override Array _GetImportOptions(string path, int presetIndex)
+ {
+ return new Array();
+ }
+
+ public override Error _Import(string sourceFile, string savePath, Dictionary options, Array platformVariants, Array genFiles)
+ {
+ Error parseResult = Error.Ok;
+ ReaderState state = ReaderState.ReadId;
+ List entries = new();
+ try
+ {
+ int currentId = 0;
+ TimeSpan startTime = TimeSpan.Zero;
+ TimeSpan endTime = TimeSpan.Zero;
+ StringBuilder content = new();
+
+ void RegisterEntry()
+ {
+ content.Replace("", "[b]").Replace("", "[/b]").Replace("{b}", "[b]").Replace("{/b}", "[/b]");
+ content.Replace("", "[i]").Replace("", "[/i]").Replace("{i}", "[i]").Replace("{/i}", "[/i]");
+ content.Replace("", "[u]").Replace("", "[/u]").Replace("{u}", "[u]").Replace("{/u}", "[/u]");
+ content.Replace("", "[/color]");
+ string contentString = content.ToString();
+ contentString = colorRegex.Replace(contentString, match => $"[color={match.Groups[1].Value}]");
+ contentString = linePositionRegex.Replace(contentString, match =>
+ {
+ string result = string.Empty;
+ if (int.TryParse(match.Groups[1].Value, out int lineCount))
+ {
+ for (int i = 1; i < lineCount; ++i)
+ {
+ result += '\n';
+ }
+ }
+ else
+ {
+ Trace.TraceError($"Can't parse subtitles line position tag: {match.Value}");
+ }
+
+ return result;
+ });
+
+ entries.Add(new SubtitleEntry()
+ {
+ Id = currentId,
+ StartTime = startTime.TotalSeconds,
+ EndTime = endTime.TotalSeconds,
+ Content = contentString,
+ });
+
+ content.Clear();
+ }
+
+ using (StreamReader reader = File.OpenText(ProjectSettings.GlobalizePath(sourceFile)))
+ {
+ while (!reader.EndOfStream)
+ {
+ string line = reader.ReadLine();
+ switch (state)
+ {
+ case ReaderState.ReadId:
+ if (!int.TryParse(line, out currentId))
+ {
+ Trace.TraceError($"Can't parse subtitles id: {line}");
+ parseResult = Error.Failed;
+ }
+
+ state = ReaderState.ReadTime;
+ break;
+
+ case ReaderState.ReadTime:
+ Match match = SubtitlesImportPlugin.timeRegex.Match(line);
+ if (!match.Success ||
+ !TimeSpan.TryParse(match.Groups["start"].Value.Replace(',', '.'), out startTime) ||
+ !TimeSpan.TryParse(match.Groups["end"].Value.Replace(',', '.'), out endTime))
+ {
+ Trace.TraceError($"Can't parse subtitles time: {line}");
+ parseResult = Error.Failed;
+ }
+
+ state = ReaderState.ReadContent;
+ break;
+
+ case ReaderState.ReadContent:
+ if (string.IsNullOrEmpty(line))
+ {
+ // End of current entry. Store it and clear buffers.
+ RegisterEntry();
+ state = ReaderState.ReadId;
+ break;
+ }
+
+ if (content.Length > 0)
+ {
+ content.Append('\n');
+ }
+
+ content.Append(line);
+ break;
+ }
+ }
+
+ // Register last entry (no need to end a line, end of file is enough)
+ if (content.Length > 0)
+ {
+ RegisterEntry();
+ }
+ }
+ }
+ catch
+ {
+ return Error.Failed;
+ }
+
+ if (parseResult != Error.Ok)
+ {
+ return parseResult;
+ }
+
+ Debug.WriteLine($"{sourceFile} parsed. Found {entries.Count} subtitles entries.");
+ entries.Sort((left, right) => left.Id.CompareTo(right.Id));
+ var subtitles = new Subtitles()
+ {
+ Entries = new Array(entries),
+ };
+
+ string filename = $"{savePath}.{this._GetSaveExtension()}";
+ return ResourceSaver.Save(subtitles, filename);
+ }
+
+ private enum ReaderState
+ {
+ ReadId,
+ ReadTime,
+ ReadContent,
+ }
+}
+
+#endif
diff --git a/addons/frog_subtitles/SubtitlesImportPlugin.cs.uid b/addons/frog_subtitles/SubtitlesImportPlugin.cs.uid
new file mode 100644
index 00000000..053a41c4
--- /dev/null
+++ b/addons/frog_subtitles/SubtitlesImportPlugin.cs.uid
@@ -0,0 +1 @@
+uid://cp8l0est28gtv
diff --git a/addons/frog_subtitles/SubtitlesPlugin.cs b/addons/frog_subtitles/SubtitlesPlugin.cs
new file mode 100644
index 00000000..bcd1571b
--- /dev/null
+++ b/addons/frog_subtitles/SubtitlesPlugin.cs
@@ -0,0 +1,34 @@
+#if TOOLS
+using Godot;
+
+namespace Frog;
+
+[Tool]
+public partial class SubtitlesPlugin : EditorPlugin
+{
+ private SubtitlesImportPlugin? importPlugin;
+
+ public override void _EnterTree()
+ {
+ Script videoStreamSubtitlesScript = GD.Load