Add initial implementation of Dialogue Manager; include core scripts, scenes, and resources
This commit is contained in:
15
Dialogue/tutorial.dialogue
Normal file
15
Dialogue/tutorial.dialogue
Normal file
@@ -0,0 +1,15 @@
|
||||
~ start
|
||||
Narrator: You are a newborn god, a consciousness adrift in the cosmos.
|
||||
|
||||
Narrator: Below you, a nascent world teems with life. A small tribe of followers has begun to worship you. Their belief is your existence.
|
||||
|
||||
Narrator: Your goal is simple: nurture this civilization until they can reach for the stars and carry you with them.
|
||||
|
||||
Narrator: Grant them miracles to grow their population and advance their industry. Click the buttons on the right to see your powers.
|
||||
|
||||
Narrator: But be warned... your divine presence is a paradox. Every miracle that helps your followers also poisons their world, corrupting it.
|
||||
|
||||
Narrator: If the world's corruption reaches 100%, it will be consumed, and you along with it.
|
||||
|
||||
Narrator: Guide them. Grow them. But do not destroy them before they can escape. Their fate, and yours, is in your hands.
|
||||
=> END
|
16
Dialogue/tutorial.dialogue.import
Normal file
16
Dialogue/tutorial.dialogue.import
Normal file
@@ -0,0 +1,16 @@
|
||||
[remap]
|
||||
|
||||
importer="dialogue_manager"
|
||||
importer_version=15
|
||||
type="Resource"
|
||||
uid="uid://dxgpvgx7axp88"
|
||||
path="res://.godot/imported/tutorial.dialogue-20e92c929aa826a6ba83a2adbbdba7f9.tres"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Dialogue/tutorial.dialogue"
|
||||
dest_files=["res://.godot/imported/tutorial.dialogue-20e92c929aa826a6ba83a2adbbdba7f9.tres"]
|
||||
|
||||
[params]
|
||||
|
||||
defaults=true
|
@@ -3,7 +3,7 @@
|
||||
[ext_resource type="Script" uid="uid://cbdokimy0qarg" path="res://Scripts/MainMenu.cs" id="1_1ehe0"]
|
||||
[ext_resource type="FontFile" uid="uid://wofoiaejxgsp" path="res://Fonts/Playful Boxes.otf" id="1_28flt"]
|
||||
[ext_resource type="Texture2D" uid="uid://d2wi2cs20q2b6" path="res://Parasitic_God.png" id="1_48xlc"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfil8sd154327" path="res://Scenes/Main/Main.tscn" id="2_ce3w2"]
|
||||
[ext_resource type="PackedScene" uid="uid://kvpk5wrcp3rv" path="res://Scenes/tutorial_scene.tscn" id="2_ce3w2"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_48xlc"]
|
||||
font = ExtResource("1_28flt")
|
||||
|
26
Scenes/tutorial_scene.tscn
Normal file
26
Scenes/tutorial_scene.tscn
Normal file
@@ -0,0 +1,26 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://kvpk5wrcp3rv"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d08d3pi7sx8k3" path="res://Scripts/UI/TutorialScene.cs" id="1_epmsy"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfil8sd154327" path="res://Scenes/Main/Main.tscn" id="2_mw53g"]
|
||||
[ext_resource type="Resource" uid="uid://dxgpvgx7axp88" path="res://Dialogue/tutorial.dialogue" id="3_oaf0i"]
|
||||
[ext_resource type="Texture2D" uid="uid://dg6ac3jb1366r" path="res://Sprites/globe.svg" id="4_7u0dx"]
|
||||
|
||||
[node name="TutorialScene" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_epmsy")
|
||||
_mainGameScene = ExtResource("2_mw53g")
|
||||
_tutorialDialogue = ExtResource("3_oaf0i")
|
||||
|
||||
[node name="World" type="TextureRect" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 195.0
|
||||
offset_top = -98.0
|
||||
offset_right = 915.0
|
||||
offset_bottom = 732.0
|
||||
texture = ExtResource("4_7u0dx")
|
||||
expand_mode = 4
|
29
Scripts/UI/TutorialScene.cs
Normal file
29
Scripts/UI/TutorialScene.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using DialogueManagerRuntime;
|
||||
using Godot;
|
||||
|
||||
namespace ParasiticGod.Scripts.UI;
|
||||
|
||||
public partial class TutorialScene : Control
|
||||
{
|
||||
[Export] private PackedScene _mainGameScene;
|
||||
[Export] private Resource _tutorialDialogue;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
DialogueManager.DialogueEnded += OnDialogueEnded;
|
||||
DialogueManager.ShowExampleDialogueBalloon(_tutorialDialogue, "start");
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
DialogueManager.DialogueEnded -= OnDialogueEnded;
|
||||
}
|
||||
|
||||
private void OnDialogueEnded(Resource resource)
|
||||
{
|
||||
if (resource == _tutorialDialogue)
|
||||
{
|
||||
GetTree().ChangeSceneToPacked(_mainGameScene);
|
||||
}
|
||||
}
|
||||
}
|
1
Scripts/UI/TutorialScene.cs.uid
Normal file
1
Scripts/UI/TutorialScene.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d08d3pi7sx8k3
|
576
addons/dialogue_manager/DialogueManager.cs
Normal file
576
addons/dialogue_manager/DialogueManager.cs
Normal file
@@ -0,0 +1,576 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
public enum TranslationSource
|
||||
{
|
||||
None,
|
||||
Guess,
|
||||
CSV,
|
||||
PO
|
||||
}
|
||||
|
||||
public partial class DialogueManager : RefCounted
|
||||
{
|
||||
public delegate void DialogueStartedEventHandler(Resource dialogueResource);
|
||||
public delegate void PassedTitleEventHandler(string title);
|
||||
public delegate void GotDialogueEventHandler(DialogueLine dialogueLine);
|
||||
public delegate void MutatedEventHandler(Dictionary mutation);
|
||||
public delegate void DialogueEndedEventHandler(Resource dialogueResource);
|
||||
|
||||
public static DialogueStartedEventHandler? DialogueStarted;
|
||||
public static PassedTitleEventHandler? PassedTitle;
|
||||
public static GotDialogueEventHandler? GotDialogue;
|
||||
public static MutatedEventHandler? Mutated;
|
||||
public static DialogueEndedEventHandler? DialogueEnded;
|
||||
|
||||
[Signal] public delegate void ResolvedEventHandler(Variant value);
|
||||
|
||||
private static GodotObject? instance;
|
||||
public static GodotObject Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = Engine.GetSingleton("DialogueManager");
|
||||
instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource)));
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Godot.Collections.Array GameStates
|
||||
{
|
||||
get => (Godot.Collections.Array)Instance.Get("game_states");
|
||||
set => Instance.Set("game_states", value);
|
||||
}
|
||||
|
||||
|
||||
public static bool IncludeSingletons
|
||||
{
|
||||
get => (bool)Instance.Get("include_singletons");
|
||||
set => Instance.Set("include_singletons", value);
|
||||
}
|
||||
|
||||
|
||||
public static bool IncludeClasses
|
||||
{
|
||||
get => (bool)Instance.Get("include_classes");
|
||||
set => Instance.Set("include_classes", value);
|
||||
}
|
||||
|
||||
|
||||
public static TranslationSource TranslationSource
|
||||
{
|
||||
get => (TranslationSource)(int)Instance.Get("translation_source");
|
||||
set => Instance.Set("translation_source", (int)value);
|
||||
}
|
||||
|
||||
|
||||
public static Func<Node> GetCurrentScene
|
||||
{
|
||||
set => Instance.Set("get_current_scene", Callable.From(value));
|
||||
}
|
||||
|
||||
|
||||
public static void Prepare(GodotObject instance)
|
||||
{
|
||||
instance.Connect("passed_title", Callable.From((string title) => PassedTitle?.Invoke(title)));
|
||||
instance.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line))));
|
||||
instance.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation)));
|
||||
instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource)));
|
||||
}
|
||||
|
||||
|
||||
public static async Task<GodotObject> GetSingleton()
|
||||
{
|
||||
if (instance != null) return instance;
|
||||
|
||||
var tree = Engine.GetMainLoop();
|
||||
int x = 0;
|
||||
|
||||
// Try and find the singleton for a few seconds
|
||||
while (!Engine.HasSingleton("DialogueManager") && x < 300)
|
||||
{
|
||||
await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame);
|
||||
x++;
|
||||
}
|
||||
|
||||
// If it times out something is wrong
|
||||
if (x >= 300)
|
||||
{
|
||||
throw new Exception("The DialogueManager singleton is missing.");
|
||||
}
|
||||
|
||||
instance = Engine.GetSingleton("DialogueManager");
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static Resource CreateResourceFromText(string text)
|
||||
{
|
||||
return (Resource)Instance.Call("create_resource_from_text", text);
|
||||
}
|
||||
|
||||
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
var instance = (Node)Instance.Call("_bridge_get_new_instance");
|
||||
Prepare(instance);
|
||||
instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
var result = await instance.ToSignal(instance, "bridge_get_next_dialogue_line_completed");
|
||||
instance.QueueFree();
|
||||
|
||||
if ((RefCounted)result[0] == null) return null;
|
||||
|
||||
return new DialogueLine((RefCounted)result[0]);
|
||||
}
|
||||
|
||||
|
||||
public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
|
||||
public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
|
||||
public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
|
||||
{
|
||||
return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
|
||||
}
|
||||
|
||||
|
||||
public static Array<string> StaticIdToLineIds(Resource dialogueResource, string staticId)
|
||||
{
|
||||
return (Array<string>)Instance.Call("static_id_to_line_ids", dialogueResource, staticId);
|
||||
}
|
||||
|
||||
|
||||
public static string StaticIdToLineId(Resource dialogueResource, string staticId)
|
||||
{
|
||||
return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId);
|
||||
}
|
||||
|
||||
|
||||
public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false)
|
||||
{
|
||||
Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation);
|
||||
await Instance.ToSignal(Instance, "bridge_mutated");
|
||||
}
|
||||
|
||||
|
||||
public static Array<Dictionary> GetMembersForAutoload(Script script)
|
||||
{
|
||||
Array<Dictionary> members = new Array<Dictionary>();
|
||||
|
||||
string typeName = script.ResourcePath.GetFile().GetBaseName();
|
||||
var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName);
|
||||
foreach (var matchingType in matchingTypes)
|
||||
{
|
||||
var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var memberInfo in memberInfos)
|
||||
{
|
||||
string type;
|
||||
switch (memberInfo.MemberType)
|
||||
{
|
||||
case MemberTypes.Field:
|
||||
FieldInfo fieldInfo = memberInfo as FieldInfo;
|
||||
|
||||
if (fieldInfo.FieldType.ToString().Contains("EventHandler"))
|
||||
{
|
||||
type = "signal";
|
||||
}
|
||||
else if (fieldInfo.IsLiteral)
|
||||
{
|
||||
type = "constant";
|
||||
}
|
||||
else
|
||||
{
|
||||
type = "property";
|
||||
}
|
||||
break;
|
||||
case MemberTypes.Method:
|
||||
type = "method";
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
members.Add(new Dictionary() {
|
||||
{ "name", memberInfo.Name },
|
||||
{ "type", type }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
|
||||
public bool ThingHasConstant(GodotObject thing, string property)
|
||||
{
|
||||
var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var fieldInfo in fieldInfos)
|
||||
{
|
||||
if (fieldInfo.Name == property && fieldInfo.IsLiteral)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public Variant ResolveThingConstant(GodotObject thing, string property)
|
||||
{
|
||||
var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var fieldInfo in fieldInfos)
|
||||
{
|
||||
if (fieldInfo.Name == property && fieldInfo.IsLiteral)
|
||||
{
|
||||
try
|
||||
{
|
||||
Variant value = fieldInfo.GetValue(thing) switch
|
||||
{
|
||||
int v => Variant.From((long)v),
|
||||
float v => Variant.From((double)v),
|
||||
System.String v => Variant.From((string)v),
|
||||
_ => Variant.From(fieldInfo.GetValue(thing))
|
||||
};
|
||||
return value;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new Exception($"Constant {property} of type ${fieldInfo.GetValue(thing).GetType()} is not supported by Variant.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"{property} is not a public constant on {thing}");
|
||||
}
|
||||
|
||||
|
||||
public bool ThingHasMethod(GodotObject thing, string method, Array<Variant> args)
|
||||
{
|
||||
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var methodInfo in methodInfos)
|
||||
{
|
||||
if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> args)
|
||||
{
|
||||
MethodInfo? info = null;
|
||||
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
|
||||
foreach (var methodInfo in methodInfos)
|
||||
{
|
||||
if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count())
|
||||
{
|
||||
info = methodInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null) return;
|
||||
|
||||
#nullable disable
|
||||
// Convert the method args to something reflection can handle
|
||||
ParameterInfo[] argTypes = info.GetParameters();
|
||||
object[] _args = new object[argTypes.Length];
|
||||
for (int i = 0; i < argTypes.Length; i++)
|
||||
{
|
||||
// check if args is assignable from derived type
|
||||
if (i < args.Count && args[i].Obj != null)
|
||||
{
|
||||
if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType()))
|
||||
{
|
||||
_args[i] = args[i].Obj;
|
||||
}
|
||||
// fallback to assigning primitive types
|
||||
else
|
||||
{
|
||||
_args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType);
|
||||
}
|
||||
}
|
||||
else if (argTypes[i].DefaultValue != null)
|
||||
{
|
||||
_args[i] = argTypes[i].DefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a single frame wait in case the method returns before signals can listen
|
||||
await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame);
|
||||
|
||||
// invoke method and handle the result based on return type
|
||||
object result = info.Invoke(thing, _args);
|
||||
|
||||
if (result is Task taskResult)
|
||||
{
|
||||
await taskResult;
|
||||
try
|
||||
{
|
||||
Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult);
|
||||
EmitSignal(SignalName.Resolved, value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
EmitSignal(SignalName.Resolved);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitSignal(SignalName.Resolved, (Variant)result);
|
||||
}
|
||||
}
|
||||
#nullable enable
|
||||
}
|
||||
|
||||
|
||||
public partial class DialogueLine : RefCounted
|
||||
{
|
||||
private string id = "";
|
||||
public string Id
|
||||
{
|
||||
get => id;
|
||||
set => id = value;
|
||||
}
|
||||
|
||||
private string type = "dialogue";
|
||||
public string Type
|
||||
{
|
||||
get => type;
|
||||
set => type = value;
|
||||
}
|
||||
|
||||
private string next_id = "";
|
||||
public string NextId
|
||||
{
|
||||
get => next_id;
|
||||
set => next_id = value;
|
||||
}
|
||||
|
||||
private string character = "";
|
||||
public string Character
|
||||
{
|
||||
get => character;
|
||||
set => character = value;
|
||||
}
|
||||
|
||||
private string text = "";
|
||||
public string Text
|
||||
{
|
||||
get => text;
|
||||
set => text = value;
|
||||
}
|
||||
|
||||
private string translation_key = "";
|
||||
public string TranslationKey
|
||||
{
|
||||
get => translation_key;
|
||||
set => translation_key = value;
|
||||
}
|
||||
|
||||
private Array<DialogueResponse> responses = new Array<DialogueResponse>();
|
||||
public Array<DialogueResponse> Responses
|
||||
{
|
||||
get => responses;
|
||||
}
|
||||
|
||||
private string? time = null;
|
||||
public string? Time
|
||||
{
|
||||
get => time;
|
||||
}
|
||||
|
||||
private Dictionary pauses = new Dictionary();
|
||||
public Dictionary Pauses
|
||||
{
|
||||
get => pauses;
|
||||
}
|
||||
|
||||
private Dictionary speeds = new Dictionary();
|
||||
public Dictionary Speeds
|
||||
{
|
||||
get => speeds;
|
||||
}
|
||||
|
||||
private Array<Godot.Collections.Array> inline_mutations = new Array<Godot.Collections.Array>();
|
||||
public Array<Godot.Collections.Array> InlineMutations
|
||||
{
|
||||
get => inline_mutations;
|
||||
}
|
||||
|
||||
private Array<DialogueLine> concurrent_lines = new Array<DialogueLine>();
|
||||
public Array<DialogueLine> ConcurrentLines
|
||||
{
|
||||
get => concurrent_lines;
|
||||
}
|
||||
|
||||
private Array<Variant> extra_game_states = new Array<Variant>();
|
||||
public Array<Variant> ExtraGameStates
|
||||
{
|
||||
get => extra_game_states;
|
||||
}
|
||||
|
||||
private Array<string> tags = new Array<string>();
|
||||
public Array<string> Tags
|
||||
{
|
||||
get => tags;
|
||||
}
|
||||
|
||||
public DialogueLine(RefCounted data)
|
||||
{
|
||||
id = (string)data.Get("id");
|
||||
type = (string)data.Get("type");
|
||||
next_id = (string)data.Get("next_id");
|
||||
character = (string)data.Get("character");
|
||||
text = (string)data.Get("text");
|
||||
translation_key = (string)data.Get("translation_key");
|
||||
pauses = (Dictionary)data.Get("pauses");
|
||||
speeds = (Dictionary)data.Get("speeds");
|
||||
inline_mutations = (Array<Godot.Collections.Array>)data.Get("inline_mutations");
|
||||
time = (string)data.Get("time");
|
||||
tags = (Array<string>)data.Get("tags");
|
||||
|
||||
foreach (var concurrent_line_data in (Array<RefCounted>)data.Get("concurrent_lines"))
|
||||
{
|
||||
concurrent_lines.Add(new DialogueLine(concurrent_line_data));
|
||||
}
|
||||
|
||||
foreach (var response in (Array<RefCounted>)data.Get("responses"))
|
||||
{
|
||||
responses.Add(new DialogueResponse(response));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string GetTagValue(string tagName)
|
||||
{
|
||||
string wrapped = $"{tagName}=";
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.StartsWith(wrapped))
|
||||
{
|
||||
return tag.Substring(wrapped.Length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "dialogue":
|
||||
return $"<DialogueLine character=\"{character}\" text=\"{text}\">";
|
||||
case "mutation":
|
||||
return "<DialogueLine mutation>";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public partial class DialogueResponse : RefCounted
|
||||
{
|
||||
private string next_id = "";
|
||||
public string NextId
|
||||
{
|
||||
get => next_id;
|
||||
set => next_id = value;
|
||||
}
|
||||
|
||||
private bool is_allowed = true;
|
||||
public bool IsAllowed
|
||||
{
|
||||
get => is_allowed;
|
||||
set => is_allowed = value;
|
||||
}
|
||||
|
||||
private string condition_as_text = "";
|
||||
public string ConditionAsText
|
||||
{
|
||||
get => condition_as_text;
|
||||
set => condition_as_text = value;
|
||||
}
|
||||
|
||||
private string text = "";
|
||||
public string Text
|
||||
{
|
||||
get => text;
|
||||
set => text = value;
|
||||
}
|
||||
|
||||
private string translation_key = "";
|
||||
public string TranslationKey
|
||||
{
|
||||
get => translation_key;
|
||||
set => translation_key = value;
|
||||
}
|
||||
|
||||
private Array<string> tags = new Array<string>();
|
||||
public Array<string> Tags
|
||||
{
|
||||
get => tags;
|
||||
}
|
||||
|
||||
public DialogueResponse(RefCounted data)
|
||||
{
|
||||
next_id = (string)data.Get("next_id");
|
||||
is_allowed = (bool)data.Get("is_allowed");
|
||||
text = (string)data.Get("text");
|
||||
translation_key = (string)data.Get("translation_key");
|
||||
tags = (Array<string>)data.Get("tags");
|
||||
}
|
||||
|
||||
public string GetTagValue(string tagName)
|
||||
{
|
||||
string wrapped = $"{tagName}=";
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.StartsWith(wrapped))
|
||||
{
|
||||
return tag.Substring(wrapped.Length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"<DialogueResponse text=\"{text}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
addons/dialogue_manager/DialogueManager.cs.uid
Normal file
1
addons/dialogue_manager/DialogueManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c4c5lsrwy3opj
|
21
addons/dialogue_manager/LICENSE
Normal file
21
addons/dialogue_manager/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-present Nathan Hoad and Dialogue Manager contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
BIN
addons/dialogue_manager/assets/banner.png
Normal file
BIN
addons/dialogue_manager/assets/banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
34
addons/dialogue_manager/assets/banner.png.import
Normal file
34
addons/dialogue_manager/assets/banner.png.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cnm67htuohhlo"
|
||||
path="res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/banner.png"
|
||||
dest_files=["res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
52
addons/dialogue_manager/assets/icon.svg
Normal file
52
addons/dialogue_manager/assets/icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.8 KiB |
38
addons/dialogue_manager/assets/icon.svg.import
Normal file
38
addons/dialogue_manager/assets/icon.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3lr2uas6ax8v"
|
||||
path="res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=true
|
52
addons/dialogue_manager/assets/responses_menu.svg
Normal file
52
addons/dialogue_manager/assets/responses_menu.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 4.2333333 4.2333335"
|
||||
version="1.1"
|
||||
id="svg291"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="responses_menu.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview293"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
width="1920px"
|
||||
units="px"
|
||||
borderlayer="true"
|
||||
inkscape:showpageshadow="false"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="45.254834"
|
||||
inkscape:cx="7.8334173"
|
||||
inkscape:cy="6.5959804"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1377"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs288" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect181"
|
||||
style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.77487;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke markers fill"
|
||||
d="M 1.5875 0.26458334 L 1.5875 0.79375001 L 4.2333334 0.79375001 L 4.2333334 0.26458334 L 1.5875 0.26458334 z M 0 0.83147381 L 0 2.4189738 L 1.3229167 1.6252238 L 0 0.83147381 z M 1.5875 1.3229167 L 1.5875 1.8520834 L 4.2333334 1.8520834 L 4.2333334 1.3229167 L 1.5875 1.3229167 z M 1.5875 2.38125 L 1.5875 2.9104167 L 4.2333334 2.9104167 L 4.2333334 2.38125 L 1.5875 2.38125 z M 1.5875 3.4395834 L 1.5875 3.9687501 L 4.2333334 3.9687501 L 4.2333334 3.4395834 L 1.5875 3.4395834 z "
|
||||
fill="#E0E0E0" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
38
addons/dialogue_manager/assets/responses_menu.svg.import
Normal file
38
addons/dialogue_manager/assets/responses_menu.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://drjfciwitjm83"
|
||||
path="res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/responses_menu.svg"
|
||||
dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=true
|
71
addons/dialogue_manager/assets/update.svg
Normal file
71
addons/dialogue_manager/assets/update.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
37
addons/dialogue_manager/assets/update.svg.import
Normal file
37
addons/dialogue_manager/assets/update.svg.import
Normal file
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3baj6rygkb3f"
|
||||
path="res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogue_manager/assets/update.svg"
|
||||
dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
1111
addons/dialogue_manager/compiler/compilation.gd
vendored
Normal file
1111
addons/dialogue_manager/compiler/compilation.gd
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
addons/dialogue_manager/compiler/compilation.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compilation.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dsgpnyqg6cprg
|
161
addons/dialogue_manager/compiler/compiled_line.gd
vendored
Normal file
161
addons/dialogue_manager/compiler/compiled_line.gd
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
## A compiled line of dialogue.
|
||||
class_name DMCompiledLine extends RefCounted
|
||||
|
||||
|
||||
## The ID of the line
|
||||
var id: String
|
||||
## The translation key (or static line ID).
|
||||
var translation_key: String = ""
|
||||
## The type of line.
|
||||
var type: String = ""
|
||||
## The character name.
|
||||
var character: String = ""
|
||||
## Any interpolation expressions for the character name.
|
||||
var character_replacements: Array[Dictionary] = []
|
||||
## The text of the line.
|
||||
var text: String = ""
|
||||
## Any interpolation expressions for the text.
|
||||
var text_replacements: Array[Dictionary] = []
|
||||
## Any response siblings associated with this line.
|
||||
var responses: PackedStringArray = []
|
||||
## Any randomise or case siblings for this line.
|
||||
var siblings: Array[Dictionary] = []
|
||||
## Any lines said simultaneously.
|
||||
var concurrent_lines: PackedStringArray = []
|
||||
## Any tags on this line.
|
||||
var tags: PackedStringArray = []
|
||||
## The condition or mutation expression for this line.
|
||||
var expression: Dictionary = {}
|
||||
## The express as the raw text that was given.
|
||||
var expression_text: String = ""
|
||||
## The next sequential line to go to after this line.
|
||||
var next_id: String = ""
|
||||
## The next line to go to after this line if it is unknown and compile time.
|
||||
var next_id_expression: Array[Dictionary] = []
|
||||
## Whether this jump line should return after the jump target sequence has ended.
|
||||
var is_snippet: bool = false
|
||||
## The ID of the next sibling line.
|
||||
var next_sibling_id: String = ""
|
||||
## The ID after this line if it belongs to a block (eg. conditions).
|
||||
var next_id_after: String = ""
|
||||
## Any doc comments attached to this line.
|
||||
var notes: String = ""
|
||||
|
||||
|
||||
#region Hooks
|
||||
|
||||
|
||||
func _init(initial_id: String, initial_type: String) -> void:
|
||||
id = initial_id
|
||||
type = initial_type
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
var s: Array = [
|
||||
"[%s]" % [type],
|
||||
"%s:" % [character] if character != "" else null,
|
||||
text if text != "" else null,
|
||||
expression if expression.size() > 0 else null,
|
||||
"[%s]" % [",".join(tags)] if tags.size() > 0 else null,
|
||||
str(siblings) if siblings.size() > 0 else null,
|
||||
str(responses) if responses.size() > 0 else null,
|
||||
"=> END" if "end" in next_id else "=> %s" % [next_id],
|
||||
"(~> %s)" % [next_sibling_id] if next_sibling_id != "" else null,
|
||||
"(==> %s)" % [next_id_after] if next_id_after != "" else null,
|
||||
].filter(func(item): return item != null)
|
||||
|
||||
return " ".join(s)
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
## Express this line as a [Dictionary] that can be stored in a resource.
|
||||
func to_data() -> Dictionary:
|
||||
var d: Dictionary = {
|
||||
id = id,
|
||||
type = type,
|
||||
next_id = next_id
|
||||
}
|
||||
|
||||
if next_id_expression.size() > 0:
|
||||
d.next_id_expression = next_id_expression
|
||||
|
||||
match type:
|
||||
DMConstants.TYPE_CONDITION:
|
||||
d.condition = expression
|
||||
if not next_sibling_id.is_empty():
|
||||
d.next_sibling_id = next_sibling_id
|
||||
d.next_id_after = next_id_after
|
||||
|
||||
DMConstants.TYPE_WHILE:
|
||||
d.condition = expression
|
||||
d.next_id_after = next_id_after
|
||||
|
||||
DMConstants.TYPE_MATCH:
|
||||
d.condition = expression
|
||||
d.next_id_after = next_id_after
|
||||
d.cases = siblings
|
||||
|
||||
DMConstants.TYPE_MUTATION:
|
||||
d.mutation = expression
|
||||
|
||||
DMConstants.TYPE_GOTO:
|
||||
d.is_snippet = is_snippet
|
||||
d.next_id_after = next_id_after
|
||||
if not siblings.is_empty():
|
||||
d.siblings = siblings
|
||||
|
||||
DMConstants.TYPE_RANDOM:
|
||||
d.siblings = siblings
|
||||
|
||||
DMConstants.TYPE_RESPONSE:
|
||||
d.text = text
|
||||
|
||||
if not responses.is_empty():
|
||||
d.responses = responses
|
||||
|
||||
if translation_key != text:
|
||||
d.translation_key = translation_key
|
||||
if not expression.is_empty():
|
||||
d.condition = expression
|
||||
if not character.is_empty():
|
||||
d.character = character
|
||||
if not character_replacements.is_empty():
|
||||
d.character_replacements = character_replacements
|
||||
if not text_replacements.is_empty():
|
||||
d.text_replacements = text_replacements
|
||||
if not tags.is_empty():
|
||||
d.tags = tags
|
||||
if not notes.is_empty():
|
||||
d.notes = notes
|
||||
if not expression_text.is_empty():
|
||||
d.condition_as_text = expression_text
|
||||
|
||||
DMConstants.TYPE_DIALOGUE:
|
||||
d.text = text
|
||||
|
||||
if translation_key != text:
|
||||
d.translation_key = translation_key
|
||||
|
||||
if not character.is_empty():
|
||||
d.character = character
|
||||
if not character_replacements.is_empty():
|
||||
d.character_replacements = character_replacements
|
||||
if not text_replacements.is_empty():
|
||||
d.text_replacements = text_replacements
|
||||
if not tags.is_empty():
|
||||
d.tags = tags
|
||||
if not notes.is_empty():
|
||||
d.notes = notes
|
||||
if not siblings.is_empty():
|
||||
d.siblings = siblings
|
||||
if not concurrent_lines.is_empty():
|
||||
d.concurrent_lines = concurrent_lines
|
||||
|
||||
return d
|
||||
|
||||
|
||||
#endregion
|
1
addons/dialogue_manager/compiler/compiled_line.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiled_line.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dg8j5hudp4210
|
51
addons/dialogue_manager/compiler/compiler.gd
vendored
Normal file
51
addons/dialogue_manager/compiler/compiler.gd
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
## A compiler of Dialogue Manager dialogue.
|
||||
class_name DMCompiler extends RefCounted
|
||||
|
||||
|
||||
## Compile a dialogue script.
|
||||
static func compile_string(text: String, path: String) -> DMCompilerResult:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
compilation.compile(text, path)
|
||||
|
||||
var result: DMCompilerResult = DMCompilerResult.new()
|
||||
result.imported_paths = compilation.imported_paths
|
||||
result.using_states = compilation.using_states
|
||||
result.character_names = compilation.character_names
|
||||
result.titles = compilation.titles
|
||||
result.first_title = compilation.first_title
|
||||
result.errors = compilation.errors
|
||||
result.lines = compilation.data
|
||||
result.raw_text = text
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Get the line type of a string. The returned string will match one of the [code]TYPE_[/code] constants of [DMConstants].
|
||||
static func get_line_type(text: String) -> String:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
return compilation.get_line_type(text)
|
||||
|
||||
|
||||
## Get the static line ID (eg. [code][ID:SOMETHING][/code]) of some text.
|
||||
static func get_static_line_id(text: String) -> String:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
return compilation.extract_static_line_id(text)
|
||||
|
||||
|
||||
## Get the translatable part of a line.
|
||||
static func extract_translatable_string(text: String) -> String:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
|
||||
var tree_line = DMTreeLine.new("")
|
||||
tree_line.text = text
|
||||
var line: DMCompiledLine = DMCompiledLine.new("", compilation.get_line_type(text))
|
||||
compilation.parse_character_and_dialogue(tree_line, line, [tree_line], 0, null)
|
||||
|
||||
return line.text
|
||||
|
||||
|
||||
## Get the known titles in a dialogue script.
|
||||
static func get_titles_in_text(text: String, path: String) -> Dictionary:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
compilation.build_line_tree(compilation.inject_imported_files(text, path))
|
||||
return compilation.titles
|
1
addons/dialogue_manager/compiler/compiler.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiler.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://chtfdmr0cqtp4
|
50
addons/dialogue_manager/compiler/compiler_regex.gd
vendored
Normal file
50
addons/dialogue_manager/compiler/compiler_regex.gd
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
## A collection of [RegEx] for use by the [DMCompiler].
|
||||
class_name DMCompilerRegEx extends RefCounted
|
||||
|
||||
|
||||
var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?<path>[^\"]+)\" as (?<prefix>[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+)")
|
||||
var USING_REGEX: RegEx = RegEx.create_from_string("^using (?<state>.*)$")
|
||||
var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+")
|
||||
var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*$")
|
||||
var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d")
|
||||
var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if|match|when) (?<expression>.*)\\:?")
|
||||
var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?<expression>.*)\\]")
|
||||
var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?<keyword>do|do!|set) (?<expression>.*)")
|
||||
var STATIC_LINE_ID_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?<id>.*?)\\]")
|
||||
var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>[\\d.]+)?( \\[if (?<condition>.+?)\\])? ")
|
||||
var GOTO_REGEX: RegEx = RegEx.create_from_string("=><? (?<goto>.*)")
|
||||
|
||||
var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
|
||||
var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.+?)\\](?<body>.*?)\\[\\/if\\]")
|
||||
|
||||
var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?<tags>.*?)\\]")
|
||||
|
||||
var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}")
|
||||
|
||||
var ALPHA_NUMERIC: RegEx = RegEx.create_from_string("[^a-zA-Z0-9\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+")
|
||||
|
||||
var TOKEN_DEFINITIONS: Dictionary = {
|
||||
DMConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\("),
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\["),
|
||||
DMConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("),
|
||||
DMConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"),
|
||||
DMConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["),
|
||||
DMConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"),
|
||||
DMConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"),
|
||||
DMConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"),
|
||||
DMConstants.TOKEN_COLON: RegEx.create_from_string("^:"),
|
||||
DMConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"),
|
||||
DMConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"),
|
||||
DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"),
|
||||
DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"),
|
||||
DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"),
|
||||
DMConstants.TOKEN_NULL_COALESCE: RegEx.create_from_string("^\\?\\."),
|
||||
DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."),
|
||||
DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"),
|
||||
DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"),
|
||||
DMConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"),
|
||||
DMConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*"),
|
||||
DMConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"),
|
||||
DMConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"),
|
||||
DMConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)")
|
||||
}
|
1
addons/dialogue_manager/compiler/compiler_regex.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiler_regex.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d3tvcrnicjibp
|
27
addons/dialogue_manager/compiler/compiler_result.gd
vendored
Normal file
27
addons/dialogue_manager/compiler/compiler_result.gd
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
## The result of using the [DMCompiler] to compile some dialogue.
|
||||
class_name DMCompilerResult extends RefCounted
|
||||
|
||||
|
||||
## Any paths that were imported into the compiled dialogue file.
|
||||
var imported_paths: PackedStringArray = []
|
||||
|
||||
## Any "using" directives.
|
||||
var using_states: PackedStringArray = []
|
||||
|
||||
## All titles in the file and the line they point to.
|
||||
var titles: Dictionary = {}
|
||||
|
||||
## The first title in the file.
|
||||
var first_title: String = ""
|
||||
|
||||
## All character names.
|
||||
var character_names: PackedStringArray = []
|
||||
|
||||
## Any compilation errors.
|
||||
var errors: Array[Dictionary] = []
|
||||
|
||||
## A map of all compiled lines.
|
||||
var lines: Dictionary = {}
|
||||
|
||||
## The raw dialogue text.
|
||||
var raw_text: String = ""
|
1
addons/dialogue_manager/compiler/compiler_result.gd.uid
Normal file
1
addons/dialogue_manager/compiler/compiler_result.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dmk74tknimqvg
|
529
addons/dialogue_manager/compiler/expression_parser.gd
vendored
Normal file
529
addons/dialogue_manager/compiler/expression_parser.gd
vendored
Normal file
@@ -0,0 +1,529 @@
|
||||
## A class for parsing a condition/mutation expression for use with the [DMCompiler].
|
||||
class_name DMExpressionParser extends RefCounted
|
||||
|
||||
|
||||
var include_comments: bool = false
|
||||
|
||||
|
||||
# Reference to the common [RegEx] that the parser needs.
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
|
||||
|
||||
## Break a string down into an expression.
|
||||
func tokenise(text: String, line_type: String, index: int) -> Array:
|
||||
var tokens: Array[Dictionary] = []
|
||||
var limit: int = 0
|
||||
while text.strip_edges() != "" and limit < 1000:
|
||||
limit += 1
|
||||
var found = _find_match(text)
|
||||
if found.size() > 0:
|
||||
tokens.append({
|
||||
index = index,
|
||||
type = found.type,
|
||||
value = found.value
|
||||
})
|
||||
index += found.value.length()
|
||||
text = found.remaining_text
|
||||
elif text.begins_with(" "):
|
||||
index += 1
|
||||
text = text.substr(1)
|
||||
else:
|
||||
return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index)
|
||||
|
||||
return _build_token_tree(tokens, line_type, "")[0]
|
||||
|
||||
|
||||
## Extract any expressions from some text
|
||||
func extract_replacements(text: String, index: int) -> Array[Dictionary]:
|
||||
var founds: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(text)
|
||||
|
||||
if founds == null or founds.size() == 0:
|
||||
return []
|
||||
|
||||
var replacements: Array[Dictionary] = []
|
||||
for found in founds:
|
||||
var replacement: Dictionary = {}
|
||||
var value_in_text: String = found.strings[0].substr(0, found.strings[0].length() - 2).substr(2)
|
||||
|
||||
# If there are closing curlie hard-up against the end of a {{...}} block then check for further
|
||||
# curlies just outside of the block.
|
||||
var text_suffix: String = text.substr(found.get_end(0))
|
||||
var expression_suffix: String = ""
|
||||
while text_suffix.begins_with("}"):
|
||||
expression_suffix += "}"
|
||||
text_suffix = text_suffix.substr(1)
|
||||
value_in_text += expression_suffix
|
||||
|
||||
var expression: Array = tokenise(value_in_text, DMConstants.TYPE_DIALOGUE, index + found.get_start(1))
|
||||
if expression.size() == 0:
|
||||
replacement = {
|
||||
index = index + found.get_start(1),
|
||||
error = DMConstants.ERR_INCOMPLETE_EXPRESSION
|
||||
}
|
||||
elif expression[0].type == DMConstants.TYPE_ERROR:
|
||||
replacement = {
|
||||
index = expression[0].i,
|
||||
error = expression[0].value
|
||||
}
|
||||
else:
|
||||
replacement = {
|
||||
value_in_text = "{{%s}}" % value_in_text,
|
||||
expression = expression
|
||||
}
|
||||
replacements.append(replacement)
|
||||
|
||||
return replacements
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
# Create a token that represents an error.
|
||||
func _build_token_tree_error(tree: Array, error: int, index: int) -> Array:
|
||||
tree.insert(0, {
|
||||
type = DMConstants.TOKEN_ERROR,
|
||||
value = error,
|
||||
i = index
|
||||
})
|
||||
return tree
|
||||
|
||||
|
||||
# Convert a list of tokens into an abstract syntax tree.
|
||||
func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array:
|
||||
var tree: Array[Dictionary] = []
|
||||
var limit = 0
|
||||
while tokens.size() > 0 and limit < 1000:
|
||||
limit += 1
|
||||
var token = tokens.pop_front()
|
||||
|
||||
var error = _check_next_token(token, tokens, line_type, expected_close_token)
|
||||
if error != OK:
|
||||
var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token
|
||||
return [_build_token_tree_error(tree, error, error_token.index), tokens]
|
||||
|
||||
match token.type:
|
||||
DMConstants.TOKEN_COMMENT:
|
||||
if include_comments:
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_COMMENT,
|
||||
value = token.value,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_FUNCTION:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_FUNCTION,
|
||||
# Consume the trailing "("
|
||||
function = token.value.substr(0, token.value.length() - 1),
|
||||
value = _tokens_to_list(sub_tree[0]),
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
var args = _tokens_to_list(sub_tree[0])
|
||||
if args.size() != 1:
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens]
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_DICTIONARY_REFERENCE,
|
||||
# Consume the trailing "["
|
||||
variable = token.value.substr(0, token.value.length() - 1),
|
||||
value = args[0],
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_BRACE_OPEN:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
var t = sub_tree[0]
|
||||
for i in range(0, t.size() - 2):
|
||||
# Convert Lua style dictionaries to string keys
|
||||
if t[i].type == DMConstants.TOKEN_VARIABLE and t[i+1].type == DMConstants.TOKEN_ASSIGNMENT:
|
||||
t[i].type = DMConstants.TOKEN_STRING
|
||||
t[i+1].type = DMConstants.TOKEN_COLON
|
||||
t[i+1].erase("value")
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_DICTIONARY,
|
||||
value = _tokens_to_dictionary(sub_tree[0]),
|
||||
i = token.index
|
||||
})
|
||||
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_BRACKET_OPEN:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
var type = DMConstants.TOKEN_ARRAY
|
||||
var value = _tokens_to_list(sub_tree[0])
|
||||
|
||||
# See if this is referencing a nested dictionary value
|
||||
if tree.size() > 0:
|
||||
var previous_token = tree[tree.size() - 1]
|
||||
if previous_token.type in [DMConstants.TOKEN_DICTIONARY_REFERENCE, DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]:
|
||||
type = DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE
|
||||
value = value[0]
|
||||
|
||||
tree.append({
|
||||
type = type,
|
||||
value = value,
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_PARENS_OPEN:
|
||||
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
||||
|
||||
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
||||
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
||||
|
||||
tree.append({
|
||||
type = DMConstants.TOKEN_GROUP,
|
||||
value = sub_tree[0],
|
||||
i = token.index
|
||||
})
|
||||
tokens = sub_tree[1]
|
||||
|
||||
DMConstants.TOKEN_PARENS_CLOSE, \
|
||||
DMConstants.TOKEN_BRACE_CLOSE, \
|
||||
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||
if token.type != expected_close_token:
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens]
|
||||
|
||||
tree.append({
|
||||
type = token.type,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
return [tree, tokens]
|
||||
|
||||
DMConstants.TOKEN_NOT:
|
||||
# Double nots negate each other
|
||||
if tokens.size() > 0 and tokens.front().type == DMConstants.TOKEN_NOT:
|
||||
tokens.pop_front()
|
||||
else:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_COMMA, \
|
||||
DMConstants.TOKEN_COLON, \
|
||||
DMConstants.TOKEN_DOT, \
|
||||
DMConstants.TOKEN_NULL_COALESCE:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_COMPARISON, \
|
||||
DMConstants.TOKEN_ASSIGNMENT, \
|
||||
DMConstants.TOKEN_OPERATOR, \
|
||||
DMConstants.TOKEN_AND_OR, \
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
var value = token.value.strip_edges()
|
||||
if value == "&&":
|
||||
value = "and"
|
||||
elif value == "||":
|
||||
value = "or"
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = value,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_STRING:
|
||||
if token.value.begins_with("&"):
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = StringName(token.value.substr(2, token.value.length() - 3)),
|
||||
i = token.index
|
||||
})
|
||||
else:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = token.value.substr(1, token.value.length() - 2),
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_CONDITION:
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token]
|
||||
|
||||
DMConstants.TOKEN_BOOL:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = token.value.to_lower() == "true",
|
||||
i = token.index
|
||||
})
|
||||
|
||||
DMConstants.TOKEN_NUMBER:
|
||||
var value = token.value.to_float() if "." in token.value else token.value.to_int()
|
||||
# If previous token is a number and this one is a negative number then
|
||||
# inject a minus operator token in between them.
|
||||
if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DMConstants.TOKEN_NUMBER:
|
||||
tree.append(({
|
||||
type = DMConstants.TOKEN_OPERATOR,
|
||||
value = "-",
|
||||
i = token.index
|
||||
}))
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = -1 * value,
|
||||
i = token.index
|
||||
})
|
||||
else:
|
||||
tree.append({
|
||||
type = token.type,
|
||||
value = value,
|
||||
i = token.index
|
||||
})
|
||||
|
||||
if expected_close_token != "":
|
||||
var index: int = tokens[0].i if tokens.size() > 0 else 0
|
||||
return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens]
|
||||
|
||||
return [tree, tokens]
|
||||
|
||||
|
||||
# Check the next token to see if it is valid to follow this one.
|
||||
func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error:
|
||||
var next_token: Dictionary = { type = null }
|
||||
if next_tokens.size() > 0:
|
||||
next_token = next_tokens.front()
|
||||
|
||||
# Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary
|
||||
# then it's an unexpected assignment in a condition line.
|
||||
if token.type == DMConstants.TOKEN_ASSIGNMENT and line_type == DMConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token):
|
||||
return DMConstants.ERR_UNEXPECTED_ASSIGNMENT
|
||||
|
||||
# Special case for a negative number after this one
|
||||
if token.type == DMConstants.TOKEN_NUMBER and next_token.type == DMConstants.TOKEN_NUMBER and next_token.value.begins_with("-"):
|
||||
return OK
|
||||
|
||||
var expected_token_types = []
|
||||
var unexpected_token_types = []
|
||||
match token.type:
|
||||
DMConstants.TOKEN_FUNCTION, \
|
||||
DMConstants.TOKEN_PARENS_OPEN:
|
||||
unexpected_token_types = [
|
||||
null,
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_COMPARISON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_BRACE_OPEN:
|
||||
expected_token_types = [
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_VARIABLE,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_BRACE_CLOSE
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_PARENS_CLOSE, \
|
||||
DMConstants.TOKEN_BRACE_CLOSE:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_COMPARISON, \
|
||||
DMConstants.TOKEN_OPERATOR, \
|
||||
DMConstants.TOKEN_DOT, \
|
||||
DMConstants.TOKEN_NULL_COALESCE, \
|
||||
DMConstants.TOKEN_NOT, \
|
||||
DMConstants.TOKEN_AND_OR, \
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||
unexpected_token_types = [
|
||||
null,
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_COMPARISON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_PARENS_CLOSE,
|
||||
DMConstants.TOKEN_BRACE_CLOSE,
|
||||
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_COMMA:
|
||||
unexpected_token_types = [
|
||||
null,
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_PARENS_CLOSE,
|
||||
DMConstants.TOKEN_BRACE_CLOSE,
|
||||
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_COLON:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_COMMA,
|
||||
DMConstants.TOKEN_COLON,
|
||||
DMConstants.TOKEN_COMPARISON,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_OPERATOR,
|
||||
DMConstants.TOKEN_AND_OR,
|
||||
DMConstants.TOKEN_PARENS_CLOSE,
|
||||
DMConstants.TOKEN_BRACE_CLOSE,
|
||||
DMConstants.TOKEN_BRACKET_CLOSE,
|
||||
DMConstants.TOKEN_DOT
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_BOOL, \
|
||||
DMConstants.TOKEN_STRING, \
|
||||
DMConstants.TOKEN_NUMBER:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_ASSIGNMENT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE,
|
||||
DMConstants.TOKEN_FUNCTION,
|
||||
DMConstants.TOKEN_PARENS_OPEN,
|
||||
DMConstants.TOKEN_BRACE_OPEN,
|
||||
DMConstants.TOKEN_BRACKET_OPEN
|
||||
]
|
||||
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
unexpected_token_types = [
|
||||
DMConstants.TOKEN_NOT,
|
||||
DMConstants.TOKEN_BOOL,
|
||||
DMConstants.TOKEN_STRING,
|
||||
DMConstants.TOKEN_NUMBER,
|
||||
DMConstants.TOKEN_VARIABLE,
|
||||
DMConstants.TOKEN_FUNCTION,
|
||||
DMConstants.TOKEN_PARENS_OPEN,
|
||||
DMConstants.TOKEN_BRACE_OPEN,
|
||||
DMConstants.TOKEN_BRACKET_OPEN
|
||||
]
|
||||
|
||||
if (expected_token_types.size() > 0 and not next_token.type in expected_token_types) \
|
||||
or (unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types):
|
||||
match next_token.type:
|
||||
null:
|
||||
return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION
|
||||
|
||||
DMConstants.TOKEN_FUNCTION:
|
||||
return DMConstants.ERR_UNEXPECTED_FUNCTION
|
||||
|
||||
DMConstants.TOKEN_PARENS_OPEN, \
|
||||
DMConstants.TOKEN_PARENS_CLOSE:
|
||||
return DMConstants.ERR_UNEXPECTED_BRACKET
|
||||
|
||||
DMConstants.TOKEN_COMPARISON, \
|
||||
DMConstants.TOKEN_ASSIGNMENT, \
|
||||
DMConstants.TOKEN_OPERATOR, \
|
||||
DMConstants.TOKEN_NOT, \
|
||||
DMConstants.TOKEN_AND_OR:
|
||||
return DMConstants.ERR_UNEXPECTED_OPERATOR
|
||||
|
||||
DMConstants.TOKEN_COMMA:
|
||||
return DMConstants.ERR_UNEXPECTED_COMMA
|
||||
DMConstants.TOKEN_COLON:
|
||||
return DMConstants.ERR_UNEXPECTED_COLON
|
||||
DMConstants.TOKEN_DOT:
|
||||
return DMConstants.ERR_UNEXPECTED_DOT
|
||||
|
||||
DMConstants.TOKEN_BOOL:
|
||||
return DMConstants.ERR_UNEXPECTED_BOOLEAN
|
||||
DMConstants.TOKEN_STRING:
|
||||
return DMConstants.ERR_UNEXPECTED_STRING
|
||||
DMConstants.TOKEN_NUMBER:
|
||||
return DMConstants.ERR_UNEXPECTED_NUMBER
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
return DMConstants.ERR_UNEXPECTED_VARIABLE
|
||||
|
||||
return DMConstants.ERR_INVALID_EXPRESSION
|
||||
|
||||
return OK
|
||||
|
||||
|
||||
# Convert a series of comma separated tokens to an [Array].
|
||||
func _tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]:
|
||||
var list: Array[Array] = []
|
||||
var current_item: Array[Dictionary] = []
|
||||
for token in tokens:
|
||||
if token.type == DMConstants.TOKEN_COMMA:
|
||||
list.append(current_item)
|
||||
current_item = []
|
||||
else:
|
||||
current_item.append(token)
|
||||
|
||||
if current_item.size() > 0:
|
||||
list.append(current_item)
|
||||
|
||||
return list
|
||||
|
||||
|
||||
# Convert a series of key/value tokens into a [Dictionary]
|
||||
func _tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary:
|
||||
var dictionary = {}
|
||||
for i in range(0, tokens.size()):
|
||||
if tokens[i].type == DMConstants.TOKEN_COLON:
|
||||
if tokens.size() == i + 2:
|
||||
dictionary[tokens[i - 1]] = tokens[i + 1]
|
||||
else:
|
||||
dictionary[tokens[i - 1]] = { type = DMConstants.TOKEN_GROUP, value = tokens.slice(i + 1), i = tokens[0].i }
|
||||
|
||||
return dictionary
|
||||
|
||||
|
||||
# Work out what the next token is from a string.
|
||||
func _find_match(input: String) -> Dictionary:
|
||||
for key in regex.TOKEN_DEFINITIONS.keys():
|
||||
var regex = regex.TOKEN_DEFINITIONS.get(key)
|
||||
var found = regex.search(input)
|
||||
if found:
|
||||
return {
|
||||
type = key,
|
||||
remaining_text = input.substr(found.strings[0].length()),
|
||||
value = found.strings[0]
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
#endregion
|
@@ -0,0 +1 @@
|
||||
uid://dbi4hbar8ubwu
|
68
addons/dialogue_manager/compiler/resolved_goto_data.gd
vendored
Normal file
68
addons/dialogue_manager/compiler/resolved_goto_data.gd
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
## Data associated with a dialogue jump/goto line.
|
||||
class_name DMResolvedGotoData extends RefCounted
|
||||
|
||||
|
||||
## The title that was specified
|
||||
var title: String = ""
|
||||
## The target line's ID
|
||||
var next_id: String = ""
|
||||
## An expression to determine the target line at runtime.
|
||||
var expression: Array[Dictionary] = []
|
||||
## The given line text with the jump syntax removed.
|
||||
var text_without_goto: String = ""
|
||||
## Whether this is a jump-and-return style jump.
|
||||
var is_snippet: bool = false
|
||||
## A parse error if there was one.
|
||||
var error: int
|
||||
## The index in the string where
|
||||
var index: int = 0
|
||||
|
||||
# An instance of the compiler [RegEx] list.
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
|
||||
|
||||
func _init(text: String, titles: Dictionary) -> void:
|
||||
if not "=> " in text and not "=>< " in text: return
|
||||
|
||||
if "=> " in text:
|
||||
text_without_goto = text.substr(0, text.find("=> ")).strip_edges()
|
||||
elif "=>< " in text:
|
||||
is_snippet = true
|
||||
text_without_goto = text.substr(0, text.find("=>< ")).strip_edges()
|
||||
|
||||
var found: RegExMatch = regex.GOTO_REGEX.search(text)
|
||||
if found == null:
|
||||
return
|
||||
|
||||
title = found.strings[found.names.goto].strip_edges()
|
||||
index = found.get_start(0)
|
||||
|
||||
if title == "":
|
||||
error = DMConstants.ERR_UNKNOWN_TITLE
|
||||
return
|
||||
|
||||
# "=> END!" means end the conversation, ignoring any "=><" chains.
|
||||
if title == "END!":
|
||||
next_id = DMConstants.ID_END_CONVERSATION
|
||||
|
||||
# "=> END" means end the current title (and go back to the previous one if there is one
|
||||
# in the stack)
|
||||
elif title == "END":
|
||||
next_id = DMConstants.ID_END
|
||||
|
||||
elif titles.has(title):
|
||||
next_id = titles.get(title)
|
||||
elif title.begins_with("{{"):
|
||||
var expression_parser: DMExpressionParser = DMExpressionParser.new()
|
||||
var title_expression: Array[Dictionary] = expression_parser.extract_replacements(title, 0)
|
||||
if title_expression[0].has("error"):
|
||||
error = title_expression[0].error
|
||||
else:
|
||||
expression = title_expression[0].expression
|
||||
else:
|
||||
next_id = title
|
||||
error = DMConstants.ERR_UNKNOWN_TITLE
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "%s =>%s %s (%s)" % [text_without_goto, "<" if is_snippet else "", title, next_id]
|
@@ -0,0 +1 @@
|
||||
uid://llhl5pt47eoq
|
167
addons/dialogue_manager/compiler/resolved_line_data.gd
vendored
Normal file
167
addons/dialogue_manager/compiler/resolved_line_data.gd
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
## Any data associated with inline dialogue BBCodes.
|
||||
class_name DMResolvedLineData extends RefCounted
|
||||
|
||||
## The line's text
|
||||
var text: String = ""
|
||||
## A map of pauses against where they are found in the text.
|
||||
var pauses: Dictionary = {}
|
||||
## A map of speed changes against where they are found in the text.
|
||||
var speeds: Dictionary = {}
|
||||
## A list of any mutations to run and where they are found in the text.
|
||||
var mutations: Array[Array] = []
|
||||
## A duration reference for the line. Represented as "auto" or a stringified number.
|
||||
var time: String = ""
|
||||
|
||||
|
||||
func _init(line: String) -> void:
|
||||
text = line
|
||||
pauses = {}
|
||||
speeds = {}
|
||||
mutations = []
|
||||
time = ""
|
||||
|
||||
var bbcodes: Array = []
|
||||
|
||||
# Remove any escaped brackets (ie. "\[")
|
||||
var escaped_open_brackets: PackedInt32Array = []
|
||||
var escaped_close_brackets: PackedInt32Array = []
|
||||
for i in range(0, text.length() - 1):
|
||||
if text.substr(i, 2) == "\\[":
|
||||
text = text.substr(0, i) + "!" + text.substr(i + 2)
|
||||
escaped_open_brackets.append(i)
|
||||
elif text.substr(i, 2) == "\\]":
|
||||
text = text.substr(0, i) + "!" + text.substr(i + 2)
|
||||
escaped_close_brackets.append(i)
|
||||
|
||||
# Extract all of the BB codes so that we know the actual text (we could do this easier with
|
||||
# a RichTextLabel but then we'd need to await idle_frame which is annoying)
|
||||
var bbcode_positions = find_bbcode_positions_in_string(text)
|
||||
var accumulaive_length_offset = 0
|
||||
for position in bbcode_positions:
|
||||
# Ignore our own markers
|
||||
if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]:
|
||||
continue
|
||||
|
||||
bbcodes.append({
|
||||
bbcode = position.bbcode,
|
||||
start = position.start,
|
||||
offset_start = position.start - accumulaive_length_offset
|
||||
})
|
||||
accumulaive_length_offset += position.bbcode.length()
|
||||
|
||||
for bb in bbcodes:
|
||||
text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length())
|
||||
|
||||
# Now find any dialogue markers
|
||||
var next_bbcode_position = find_bbcode_positions_in_string(text, false)
|
||||
var limit = 0
|
||||
while next_bbcode_position.size() > 0 and limit < 1000:
|
||||
limit += 1
|
||||
|
||||
var bbcode = next_bbcode_position[0]
|
||||
|
||||
var index = bbcode.start
|
||||
var code = bbcode.code
|
||||
var raw_args = bbcode.raw_args
|
||||
var args = {}
|
||||
if code in ["do", "do!", "set"]:
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
args["value"] = compilation.extract_mutation("%s %s" % [code, raw_args])
|
||||
else:
|
||||
# Could be something like:
|
||||
# "=1.0"
|
||||
# " rate=20 level=10"
|
||||
if raw_args and raw_args[0] == "=":
|
||||
raw_args = "value" + raw_args
|
||||
for pair in raw_args.strip_edges().split(" "):
|
||||
if "=" in pair:
|
||||
var bits = pair.split("=")
|
||||
args[bits[0]] = bits[1]
|
||||
|
||||
match code:
|
||||
"wait":
|
||||
if pauses.has(index):
|
||||
pauses[index] += args.get("value").to_float()
|
||||
else:
|
||||
pauses[index] = args.get("value").to_float()
|
||||
"speed":
|
||||
speeds[index] = args.get("value").to_float()
|
||||
"/speed":
|
||||
speeds[index] = 1.0
|
||||
"do", "do!", "set":
|
||||
mutations.append([index, args.get("value")])
|
||||
"next":
|
||||
time = args.get("value") if args.has("value") else "0"
|
||||
|
||||
# Find any BB codes that are after this index and remove the length from their start
|
||||
var length = bbcode.bbcode.length()
|
||||
for bb in bbcodes:
|
||||
if bb.offset_start > bbcode.start:
|
||||
bb.offset_start -= length
|
||||
bb.start -= length
|
||||
|
||||
# Find any escaped brackets after this that need moving
|
||||
for i in range(0, escaped_open_brackets.size()):
|
||||
if escaped_open_brackets[i] > bbcode.start:
|
||||
escaped_open_brackets[i] -= length
|
||||
for i in range(0, escaped_close_brackets.size()):
|
||||
if escaped_close_brackets[i] > bbcode.start:
|
||||
escaped_close_brackets[i] -= length
|
||||
|
||||
text = text.substr(0, index) + text.substr(index + length)
|
||||
next_bbcode_position = find_bbcode_positions_in_string(text, false)
|
||||
|
||||
# Put the BB Codes back in
|
||||
for bb in bbcodes:
|
||||
text = text.insert(bb.start, bb.bbcode)
|
||||
|
||||
# Put the escaped brackets back in
|
||||
for index in escaped_open_brackets:
|
||||
text = text.left(index) + "[" + text.right(text.length() - index - 1)
|
||||
for index in escaped_close_brackets:
|
||||
text = text.left(index) + "]" + text.right(text.length() - index - 1)
|
||||
|
||||
|
||||
func find_bbcode_positions_in_string(string: String, find_all: bool = true, include_conditions: bool = false) -> Array[Dictionary]:
|
||||
if not "[" in string: return []
|
||||
|
||||
var positions: Array[Dictionary] = []
|
||||
|
||||
var open_brace_count: int = 0
|
||||
var start: int = 0
|
||||
var bbcode: String = ""
|
||||
var code: String = ""
|
||||
var is_finished_code: bool = false
|
||||
for i in range(0, string.length()):
|
||||
if string[i] == "[":
|
||||
if open_brace_count == 0:
|
||||
start = i
|
||||
bbcode = ""
|
||||
code = ""
|
||||
is_finished_code = false
|
||||
open_brace_count += 1
|
||||
|
||||
else:
|
||||
if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"):
|
||||
code += string[i]
|
||||
else:
|
||||
is_finished_code = true
|
||||
|
||||
if open_brace_count > 0:
|
||||
bbcode += string[i]
|
||||
|
||||
if string[i] == "]":
|
||||
open_brace_count -= 1
|
||||
if open_brace_count == 0 and (include_conditions or not code in ["if", "else", "/if"]):
|
||||
positions.append({
|
||||
bbcode = bbcode,
|
||||
code = code,
|
||||
start = start,
|
||||
end = i,
|
||||
raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges()
|
||||
})
|
||||
|
||||
if not find_all:
|
||||
return positions
|
||||
|
||||
return positions
|
@@ -0,0 +1 @@
|
||||
uid://0k6q8kukq0qa
|
26
addons/dialogue_manager/compiler/resolved_tag_data.gd
vendored
Normal file
26
addons/dialogue_manager/compiler/resolved_tag_data.gd
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
## Tag data associated with a line of dialogue.
|
||||
class_name DMResolvedTagData extends RefCounted
|
||||
|
||||
|
||||
## The list of tags.
|
||||
var tags: PackedStringArray = []
|
||||
## The line with any tag syntax removed.
|
||||
var text_without_tags: String = ""
|
||||
|
||||
# An instance of the compiler [RegEx].
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
|
||||
|
||||
func _init(text: String) -> void:
|
||||
var resolved_tags: PackedStringArray = []
|
||||
var tag_matches: Array[RegExMatch] = regex.TAGS_REGEX.search_all(text)
|
||||
for tag_match in tag_matches:
|
||||
text = text.replace(tag_match.get_string(), "")
|
||||
var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",")
|
||||
for tag in tags:
|
||||
tag = tag.replace("#", "")
|
||||
if not tag in resolved_tags:
|
||||
resolved_tags.append(tag)
|
||||
|
||||
tags = resolved_tags
|
||||
text_without_tags = text
|
@@ -0,0 +1 @@
|
||||
uid://cqai3ikuilqfq
|
46
addons/dialogue_manager/compiler/tree_line.gd
vendored
Normal file
46
addons/dialogue_manager/compiler/tree_line.gd
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
## An intermediate representation of a dialogue line before it gets compiled.
|
||||
class_name DMTreeLine extends RefCounted
|
||||
|
||||
|
||||
## The line number where this dialogue was found (after imported files have had their content imported).
|
||||
var line_number: int = 0
|
||||
## The parent [DMTreeLine] of this line.
|
||||
## This is stored as a Weak Reference so that this RefCounted can elegantly free itself.
|
||||
## Without it being a Weak Reference, this can easily cause a cyclical reference that keeps this resource alive.
|
||||
var parent: WeakRef
|
||||
## The ID of this line.
|
||||
var id: String
|
||||
## The type of this line (as a [String] defined in [DMConstants].
|
||||
var type: String = ""
|
||||
## Is this line part of a randomised group?
|
||||
var is_random: bool = false
|
||||
## The indent count for this line.
|
||||
var indent: int = 0
|
||||
## The text of this line.
|
||||
var text: String = ""
|
||||
## The child [DMTreeLine]s of this line.
|
||||
var children: Array[DMTreeLine] = []
|
||||
## Any doc comments attached to this line.
|
||||
var notes: String = ""
|
||||
## Is this a dialogue line that is the child of another dialogue line?
|
||||
var is_nested_dialogue: bool = false
|
||||
|
||||
|
||||
func _init(initial_id: String) -> void:
|
||||
id = initial_id
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
var tabs = []
|
||||
tabs.resize(indent)
|
||||
tabs.fill("\t")
|
||||
tabs = "".join(tabs)
|
||||
|
||||
return tabs.join([tabs + "{\n",
|
||||
"\tid: %s\n" % [id],
|
||||
"\ttype: %s\n" % [type],
|
||||
"\tis_random: %s\n" % ["true" if is_random else "false"],
|
||||
"\ttext: %s\n" % [text],
|
||||
"\tnotes: %s\n" % [notes],
|
||||
"\tchildren: []\n" if children.size() == 0 else "\tchildren: [\n" + ",\n".join(children.map(func(child): return str(child))) + "]\n",
|
||||
"}"])
|
1
addons/dialogue_manager/compiler/tree_line.gd.uid
Normal file
1
addons/dialogue_manager/compiler/tree_line.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dsu4i84dpif14
|
610
addons/dialogue_manager/components/code_edit.gd
vendored
Normal file
610
addons/dialogue_manager/components/code_edit.gd
vendored
Normal file
@@ -0,0 +1,610 @@
|
||||
@tool
|
||||
class_name DMCodeEdit extends CodeEdit
|
||||
|
||||
|
||||
signal active_title_change(title: String)
|
||||
signal error_clicked(line_number: int)
|
||||
signal external_file_requested(path: String, title: String)
|
||||
|
||||
|
||||
# A link back to the owner `MainView`
|
||||
var main_view
|
||||
|
||||
# Theme overrides for syntax highlighting, etc
|
||||
var theme_overrides: Dictionary:
|
||||
set(value):
|
||||
theme_overrides = value
|
||||
|
||||
syntax_highlighter = DMSyntaxHighlighter.new()
|
||||
|
||||
# General UI
|
||||
add_theme_color_override("font_color", theme_overrides.text_color)
|
||||
add_theme_color_override("background_color", theme_overrides.background_color)
|
||||
add_theme_color_override("current_line_color", theme_overrides.current_line_color)
|
||||
add_theme_font_override("font", get_theme_font("source", "EditorFonts"))
|
||||
add_theme_font_size_override("font_size", theme_overrides.font_size * theme_overrides.scale)
|
||||
font_size = round(theme_overrides.font_size)
|
||||
get:
|
||||
return theme_overrides
|
||||
|
||||
# Any parse errors
|
||||
var errors: Array:
|
||||
set(next_errors):
|
||||
errors = next_errors
|
||||
for i in range(0, get_line_count()):
|
||||
var is_error: bool = false
|
||||
for error in errors:
|
||||
if error.line_number == i:
|
||||
is_error = true
|
||||
mark_line_as_error(i, is_error)
|
||||
_on_code_edit_caret_changed()
|
||||
get:
|
||||
return errors
|
||||
|
||||
# The last selection (if there was one) so we can remember it for refocusing
|
||||
var last_selected_text: String
|
||||
|
||||
var font_size: int:
|
||||
set(value):
|
||||
font_size = value
|
||||
add_theme_font_size_override("font_size", font_size * theme_overrides.scale)
|
||||
get:
|
||||
return font_size
|
||||
|
||||
var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s")
|
||||
|
||||
var compiler_regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
var _autoloads: Dictionary[String, String] = {}
|
||||
var _autoload_member_cache: Dictionary[String, Dictionary] = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Add error gutter
|
||||
add_gutter(0)
|
||||
set_gutter_type(0, TextEdit.GUTTER_TYPE_ICON)
|
||||
|
||||
# Add comment delimiter
|
||||
if not has_comment_delimiter("#"):
|
||||
add_comment_delimiter("#", "", true)
|
||||
|
||||
syntax_highlighter = DMSyntaxHighlighter.new()
|
||||
|
||||
# Keep track of any autoloads
|
||||
ProjectSettings.settings_changed.connect(_on_project_settings_changed)
|
||||
_on_project_settings_changed()
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
# Handle shortcuts that come from the editor
|
||||
if event is InputEventKey and event.is_pressed():
|
||||
var shortcut: String = Engine.get_meta("DialogueManagerPlugin").get_editor_shortcut(event)
|
||||
match shortcut:
|
||||
"toggle_comment":
|
||||
toggle_comment()
|
||||
get_viewport().set_input_as_handled()
|
||||
"delete_line":
|
||||
delete_current_line()
|
||||
get_viewport().set_input_as_handled()
|
||||
"move_up":
|
||||
move_line(-1)
|
||||
get_viewport().set_input_as_handled()
|
||||
"move_down":
|
||||
move_line(1)
|
||||
get_viewport().set_input_as_handled()
|
||||
"text_size_increase":
|
||||
self.font_size += 1
|
||||
get_viewport().set_input_as_handled()
|
||||
"text_size_decrease":
|
||||
self.font_size -= 1
|
||||
get_viewport().set_input_as_handled()
|
||||
"text_size_reset":
|
||||
self.font_size = theme_overrides.font_size
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
elif event is InputEventMouse:
|
||||
match event.as_text():
|
||||
"Ctrl+Mouse Wheel Up", "Command+Mouse Wheel Up":
|
||||
self.font_size += 1
|
||||
get_viewport().set_input_as_handled()
|
||||
"Ctrl+Mouse Wheel Down", "Command+Mouse Wheel Down":
|
||||
self.font_size -= 1
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _can_drop_data(at_position: Vector2, data) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY: return false
|
||||
if data.type != "files": return false
|
||||
|
||||
var files: PackedStringArray = Array(data.files)
|
||||
return files.size() > 0
|
||||
|
||||
|
||||
func _drop_data(at_position: Vector2, data) -> void:
|
||||
var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+")
|
||||
|
||||
var files: PackedStringArray = Array(data.files)
|
||||
for file in files:
|
||||
# Don't import the file into itself
|
||||
if file == main_view.current_file_path: continue
|
||||
|
||||
if file.get_extension() == "dialogue":
|
||||
var path = file.replace("res://", "").replace(".dialogue", "")
|
||||
# Find the first non-import line in the file to add our import
|
||||
var lines = text.split("\n")
|
||||
for i in range(0, lines.size()):
|
||||
if not lines[i].begins_with("import "):
|
||||
insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)])
|
||||
set_caret_line(i)
|
||||
break
|
||||
else:
|
||||
var cursor: Vector2 = get_line_column_at_pos(at_position)
|
||||
if cursor.x > -1 and cursor.y > -1:
|
||||
set_cursor(cursor)
|
||||
remove_secondary_carets()
|
||||
insert_text("\"%s\"" % file, cursor.y, cursor.x)
|
||||
grab_focus()
|
||||
|
||||
|
||||
func _request_code_completion(force: bool) -> void:
|
||||
var cursor: Vector2 = get_cursor()
|
||||
var current_line: String = get_line(cursor.y)
|
||||
|
||||
# Match jumps
|
||||
if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")):
|
||||
var prompt: String = current_line.split("=>")[1]
|
||||
if prompt.begins_with("< "):
|
||||
prompt = prompt.substr(2)
|
||||
else:
|
||||
prompt = prompt.substr(1)
|
||||
|
||||
if "=> " in current_line:
|
||||
if matches_prompt(prompt, "end"):
|
||||
add_code_completion_option(CodeEdit.KIND_CLASS, "END", "END".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
|
||||
if matches_prompt(prompt, "end!"):
|
||||
add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
|
||||
|
||||
# Get all titles, including those in imports
|
||||
for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path):
|
||||
# Ignore any imported titles that aren't resolved to human readable.
|
||||
if title.to_int() > 0:
|
||||
continue
|
||||
|
||||
elif "/" in title:
|
||||
var bits = title.split("/")
|
||||
if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]):
|
||||
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons"))
|
||||
elif matches_prompt(prompt, title):
|
||||
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons"))
|
||||
|
||||
# Match character names
|
||||
var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "")
|
||||
if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]:
|
||||
# Only show names starting with that character
|
||||
var names: PackedStringArray = get_character_names(name_so_far)
|
||||
if names.size() > 0:
|
||||
for name in names:
|
||||
add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons"))
|
||||
|
||||
# Match autoloads on mutation lines
|
||||
for prefix in ["do ", "do! ", "set ", "if ", "elif ", "else if ", "match ", "when ", "using "]:
|
||||
if (current_line.strip_edges().begins_with(prefix) and (cursor.x > current_line.find(prefix))):
|
||||
var expression: String = current_line.substr(0, cursor.x).strip_edges().substr(3)
|
||||
# Find the last couple of tokens
|
||||
var possible_prompt: String = expression.reverse()
|
||||
possible_prompt = possible_prompt.substr(0, possible_prompt.find(" "))
|
||||
possible_prompt = possible_prompt.substr(0, possible_prompt.find("("))
|
||||
possible_prompt = possible_prompt.reverse()
|
||||
var segments: PackedStringArray = possible_prompt.split(".").slice(-2)
|
||||
var auto_completes: Array[Dictionary] = []
|
||||
|
||||
# Autoloads and state shortcuts
|
||||
if segments.size() == 1:
|
||||
var prompt: String = segments[0]
|
||||
for autoload in _autoloads.keys():
|
||||
if matches_prompt(prompt, autoload):
|
||||
auto_completes.append({
|
||||
prompt = prompt,
|
||||
text = autoload,
|
||||
type = "script"
|
||||
})
|
||||
for autoload in get_state_shortcuts():
|
||||
for member: Dictionary in get_members_for_autoload(autoload):
|
||||
if matches_prompt(prompt, member.name):
|
||||
auto_completes.append({
|
||||
prompt = prompt,
|
||||
text = member.name,
|
||||
type = member.type
|
||||
})
|
||||
|
||||
# Members of an autoload
|
||||
elif segments[0] in _autoloads.keys() and not current_line.strip_edges().begins_with("using "):
|
||||
var prompt: String = segments[1]
|
||||
for member: Dictionary in get_members_for_autoload(segments[0]):
|
||||
if matches_prompt(prompt, member.name):
|
||||
auto_completes.append({
|
||||
prompt = prompt,
|
||||
text = member.name,
|
||||
type = member.type
|
||||
})
|
||||
|
||||
auto_completes.sort_custom(func(a, b): return a.text < b.text)
|
||||
|
||||
for auto_complete in auto_completes:
|
||||
var icon: Texture2D
|
||||
var text: String = auto_complete.text
|
||||
match auto_complete.type:
|
||||
"script":
|
||||
icon = get_theme_icon("Script", "EditorIcons")
|
||||
"property":
|
||||
icon = get_theme_icon("MemberProperty", "EditorIcons")
|
||||
"method":
|
||||
icon = get_theme_icon("MemberMethod", "EditorIcons")
|
||||
text += "()"
|
||||
"signal":
|
||||
icon = get_theme_icon("MemberSignal", "EditorIcons")
|
||||
"constant":
|
||||
icon = get_theme_icon("MemberConstant", "EditorIcons")
|
||||
var insert: String = text.substr(auto_complete.prompt.length())
|
||||
add_code_completion_option(CodeEdit.KIND_CLASS, text, insert, theme_overrides.text_color, icon)
|
||||
|
||||
update_code_completion_options(true)
|
||||
if get_code_completion_options().size() == 0:
|
||||
cancel_code_completion()
|
||||
|
||||
|
||||
func _filter_code_completion_candidates(candidates: Array) -> Array:
|
||||
# Not sure why but if this method isn't overridden then all completions are wrapped in quotes.
|
||||
return candidates
|
||||
|
||||
|
||||
func _confirm_code_completion(replace: bool) -> void:
|
||||
var completion = get_code_completion_option(get_code_completion_selected_index())
|
||||
begin_complex_operation()
|
||||
# Delete any part of the text that we've already typed
|
||||
if completion.insert_text.length() > 0:
|
||||
for i in range(0, completion.display_text.length() - completion.insert_text.length()):
|
||||
backspace()
|
||||
# Insert the whole match
|
||||
insert_text_at_caret(completion.display_text)
|
||||
end_complex_operation()
|
||||
|
||||
if completion.display_text.ends_with("()"):
|
||||
set_cursor(get_cursor() - Vector2.RIGHT)
|
||||
|
||||
# Close the autocomplete menu on the next tick
|
||||
call_deferred("cancel_code_completion")
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
# Get the current caret as a Vector2
|
||||
func get_cursor() -> Vector2:
|
||||
return Vector2(get_caret_column(), get_caret_line())
|
||||
|
||||
|
||||
# Set the caret from a Vector2
|
||||
func set_cursor(from_cursor: Vector2) -> void:
|
||||
set_caret_line(from_cursor.y, false)
|
||||
set_caret_column(from_cursor.x, false)
|
||||
|
||||
|
||||
# Check if a prompt is the start of a string without actually being that string
|
||||
func matches_prompt(prompt: String, matcher: String) -> bool:
|
||||
return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower())
|
||||
|
||||
|
||||
func get_state_shortcuts() -> PackedStringArray:
|
||||
# Get any shortcuts defined in settings
|
||||
var shortcuts: PackedStringArray = DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, [])
|
||||
# Check for "using" clauses
|
||||
for line: String in text.split("\n"):
|
||||
var found: RegExMatch = compiler_regex.USING_REGEX.search(line)
|
||||
if found:
|
||||
shortcuts.append(found.strings[found.names.state])
|
||||
# Check for any other script sources
|
||||
for extra_script_source in DMSettings.get_setting(DMSettings.EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES, []):
|
||||
shortcuts.append(extra_script_source)
|
||||
|
||||
return shortcuts
|
||||
|
||||
|
||||
func get_members_for_autoload(autoload_name: String) -> Array[Dictionary]:
|
||||
# Debounce method list lookups
|
||||
if _autoload_member_cache.has(autoload_name) and _autoload_member_cache.get(autoload_name).get("at") > Time.get_ticks_msec() - 5000:
|
||||
return _autoload_member_cache.get(autoload_name).get("members")
|
||||
|
||||
if not _autoloads.has(autoload_name) and not autoload_name.begins_with("res://") and not autoload_name.begins_with("uid://"): return []
|
||||
|
||||
var autoload = load(_autoloads.get(autoload_name, autoload_name))
|
||||
var script: Script = autoload if autoload is Script else autoload.get_script()
|
||||
|
||||
if not is_instance_valid(script): return []
|
||||
|
||||
var members: Array[Dictionary] = []
|
||||
if script.resource_path.ends_with(".gd"):
|
||||
for m: Dictionary in script.get_script_method_list():
|
||||
if not m.name.begins_with("@"):
|
||||
members.append({
|
||||
name = m.name,
|
||||
type = "method"
|
||||
})
|
||||
for m: Dictionary in script.get_script_property_list():
|
||||
members.append({
|
||||
name = m.name,
|
||||
type = "property"
|
||||
})
|
||||
for m: Dictionary in script.get_script_signal_list():
|
||||
members.append({
|
||||
name = m.name,
|
||||
type = "signal"
|
||||
})
|
||||
for c: String in script.get_script_constant_map():
|
||||
members.append({
|
||||
name = c,
|
||||
type = "constant"
|
||||
})
|
||||
elif script.resource_path.ends_with(".cs"):
|
||||
var dotnet = load(Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + "/DialogueManager.cs").new()
|
||||
for m: Dictionary in dotnet.GetMembersForAutoload(script):
|
||||
members.append(m)
|
||||
|
||||
_autoload_member_cache[autoload_name] = {
|
||||
at = Time.get_ticks_msec(),
|
||||
members = members
|
||||
}
|
||||
|
||||
return members
|
||||
|
||||
|
||||
## Get a list of titles from the current text
|
||||
func get_titles() -> PackedStringArray:
|
||||
var titles = PackedStringArray([])
|
||||
var lines = text.split("\n")
|
||||
for line in lines:
|
||||
if line.strip_edges().begins_with("~ "):
|
||||
titles.append(line.strip_edges().substr(2))
|
||||
|
||||
return titles
|
||||
|
||||
|
||||
## Work out what the next title above the current line is
|
||||
func check_active_title() -> void:
|
||||
var line_number = get_caret_line()
|
||||
var lines = text.split("\n")
|
||||
# Look at each line above this one to find the next title line
|
||||
for i in range(line_number, -1, -1):
|
||||
if lines[i].begins_with("~ "):
|
||||
active_title_change.emit(lines[i].replace("~ ", ""))
|
||||
return
|
||||
|
||||
active_title_change.emit("")
|
||||
|
||||
|
||||
# Move the caret line to match a given title
|
||||
func go_to_title(title: String) -> void:
|
||||
var lines = text.split("\n")
|
||||
for i in range(0, lines.size()):
|
||||
if lines[i].strip_edges() == "~ " + title:
|
||||
set_caret_line(i)
|
||||
center_viewport_to_caret()
|
||||
|
||||
|
||||
func get_character_names(beginning_with: String) -> PackedStringArray:
|
||||
var names: PackedStringArray = []
|
||||
var lines = text.split("\n")
|
||||
for line in lines:
|
||||
if ": " in line:
|
||||
var name: String = WEIGHTED_RANDOM_PREFIX.sub(line.split(": ")[0].strip_edges(), "")
|
||||
if not name in names and matches_prompt(beginning_with, name):
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
# Mark a line as an error or not
|
||||
func mark_line_as_error(line_number: int, is_error: bool) -> void:
|
||||
# Lines display counting from 1 but are actually indexed from 0
|
||||
line_number -= 1
|
||||
|
||||
if line_number < 0: return
|
||||
|
||||
if is_error:
|
||||
set_line_background_color(line_number, theme_overrides.error_line_color)
|
||||
set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons"))
|
||||
else:
|
||||
set_line_background_color(line_number, theme_overrides.background_color)
|
||||
set_line_gutter_icon(line_number, 0, null)
|
||||
|
||||
|
||||
# Insert or wrap some bbcode at the caret/selection
|
||||
func insert_bbcode(open_tag: String, close_tag: String = "") -> void:
|
||||
if close_tag == "":
|
||||
insert_text_at_caret(open_tag)
|
||||
grab_focus()
|
||||
else:
|
||||
var selected_text = get_selected_text()
|
||||
insert_text_at_caret("%s%s%s" % [open_tag, selected_text, close_tag])
|
||||
grab_focus()
|
||||
set_caret_column(get_caret_column() - close_tag.length())
|
||||
|
||||
# Insert text at current caret position
|
||||
# Move Caret down 1 line if not => END
|
||||
func insert_text_at_cursor(text: String) -> void:
|
||||
if text != "=> END":
|
||||
insert_text_at_caret(text+"\n")
|
||||
set_caret_line(get_caret_line()+1)
|
||||
else:
|
||||
insert_text_at_caret(text)
|
||||
grab_focus()
|
||||
|
||||
|
||||
# Toggle the selected lines as comments
|
||||
func toggle_comment() -> void:
|
||||
begin_complex_operation()
|
||||
|
||||
var comment_delimiter: String = delimiter_comments[0]
|
||||
var is_first_line: bool = true
|
||||
var will_comment: bool = true
|
||||
var selections: Array = []
|
||||
var line_offsets: Dictionary = {}
|
||||
|
||||
for caret_index in range(0, get_caret_count()):
|
||||
var from_line: int = get_caret_line(caret_index)
|
||||
var from_column: int = get_caret_column(caret_index)
|
||||
var to_line: int = get_caret_line(caret_index)
|
||||
var to_column: int = get_caret_column(caret_index)
|
||||
|
||||
if has_selection(caret_index):
|
||||
from_line = get_selection_from_line(caret_index)
|
||||
to_line = get_selection_to_line(caret_index)
|
||||
from_column = get_selection_from_column(caret_index)
|
||||
to_column = get_selection_to_column(caret_index)
|
||||
|
||||
selections.append({
|
||||
from_line = from_line,
|
||||
from_column = from_column,
|
||||
to_line = to_line,
|
||||
to_column = to_column
|
||||
})
|
||||
|
||||
for line_number in range(from_line, to_line + 1):
|
||||
if line_offsets.has(line_number): continue
|
||||
|
||||
var line_text: String = get_line(line_number)
|
||||
|
||||
# The first line determines if we are commenting or uncommentingg
|
||||
if is_first_line:
|
||||
is_first_line = false
|
||||
will_comment = not line_text.strip_edges().begins_with(comment_delimiter)
|
||||
|
||||
# Only comment/uncomment if the current line needs to
|
||||
if will_comment:
|
||||
set_line(line_number, comment_delimiter + line_text)
|
||||
line_offsets[line_number] = 1
|
||||
elif line_text.begins_with(comment_delimiter):
|
||||
set_line(line_number, line_text.substr(comment_delimiter.length()))
|
||||
line_offsets[line_number] = -1
|
||||
else:
|
||||
line_offsets[line_number] = 0
|
||||
|
||||
for caret_index in range(0, get_caret_count()):
|
||||
var selection: Dictionary = selections[caret_index]
|
||||
select(
|
||||
selection.from_line,
|
||||
selection.from_column + line_offsets[selection.from_line],
|
||||
selection.to_line,
|
||||
selection.to_column + line_offsets[selection.to_line],
|
||||
caret_index
|
||||
)
|
||||
set_caret_column(selection.from_column + line_offsets[selection.from_line], false, caret_index)
|
||||
|
||||
end_complex_operation()
|
||||
|
||||
text_set.emit()
|
||||
text_changed.emit()
|
||||
|
||||
|
||||
# Remove the current line
|
||||
func delete_current_line() -> void:
|
||||
var cursor = get_cursor()
|
||||
if get_line_count() == 1:
|
||||
select_all()
|
||||
elif cursor.y == 0:
|
||||
select(0, 0, 1, 0)
|
||||
else:
|
||||
select(cursor.y - 1, get_line_width(cursor.y - 1), cursor.y, get_line_width(cursor.y))
|
||||
delete_selection()
|
||||
text_changed.emit()
|
||||
|
||||
|
||||
# Move the selected lines up or down
|
||||
func move_line(offset: int) -> void:
|
||||
offset = clamp(offset, -1, 1)
|
||||
|
||||
var starting_scroll := scroll_vertical
|
||||
var cursor = get_cursor()
|
||||
var reselect: bool = false
|
||||
var from: int = cursor.y
|
||||
var to: int = cursor.y
|
||||
if has_selection():
|
||||
reselect = true
|
||||
from = get_selection_from_line()
|
||||
to = get_selection_to_line()
|
||||
|
||||
var lines := text.split("\n")
|
||||
|
||||
# Prevent the lines from being out of bounds
|
||||
if from + offset < 0 or to + offset >= lines.size(): return
|
||||
|
||||
var target_from_index = from - 1 if offset == -1 else to + 1
|
||||
var target_to_index = to if offset == -1 else from
|
||||
var line_to_move = lines[target_from_index]
|
||||
lines.remove_at(target_from_index)
|
||||
lines.insert(target_to_index, line_to_move)
|
||||
|
||||
text = "\n".join(lines)
|
||||
|
||||
cursor.y += offset
|
||||
set_cursor(cursor)
|
||||
from += offset
|
||||
to += offset
|
||||
if reselect:
|
||||
select(from, 0, to, get_line_width(to))
|
||||
|
||||
text_changed.emit()
|
||||
scroll_vertical = starting_scroll + offset
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_project_settings_changed() -> void:
|
||||
_autoloads = {}
|
||||
var project = ConfigFile.new()
|
||||
project.load("res://project.godot")
|
||||
for autoload in project.get_section_keys("autoload"):
|
||||
if autoload != "DialogueManager":
|
||||
_autoloads[autoload] = project.get_value("autoload", autoload).substr(1)
|
||||
|
||||
|
||||
func _on_code_edit_symbol_validate(symbol: String) -> void:
|
||||
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
|
||||
set_symbol_lookup_word_as_valid(true)
|
||||
return
|
||||
|
||||
for title in get_titles():
|
||||
if symbol == title:
|
||||
set_symbol_lookup_word_as_valid(true)
|
||||
return
|
||||
set_symbol_lookup_word_as_valid(false)
|
||||
|
||||
|
||||
func _on_code_edit_symbol_lookup(symbol: String, line: int, column: int) -> void:
|
||||
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
|
||||
external_file_requested.emit(symbol, "")
|
||||
else:
|
||||
go_to_title(symbol)
|
||||
|
||||
|
||||
func _on_code_edit_text_changed() -> void:
|
||||
request_code_completion(true)
|
||||
|
||||
|
||||
func _on_code_edit_text_set() -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_code_edit_caret_changed() -> void:
|
||||
check_active_title()
|
||||
last_selected_text = get_selected_text()
|
||||
|
||||
|
||||
func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void:
|
||||
var line_errors = errors.filter(func(error): return error.line_number == line)
|
||||
if line_errors.size() > 0:
|
||||
error_clicked.emit(line)
|
||||
|
||||
|
||||
#endregion
|
1
addons/dialogue_manager/components/code_edit.gd.uid
Normal file
1
addons/dialogue_manager/components/code_edit.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://djeybvlb332mp
|
56
addons/dialogue_manager/components/code_edit.tscn
Normal file
56
addons/dialogue_manager/components/code_edit.tscn
Normal file
@@ -0,0 +1,56 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"]
|
||||
[ext_resource type="Script" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"]
|
||||
|
||||
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"]
|
||||
script = ExtResource("1_58cfo")
|
||||
|
||||
[node name="CodeEdit" type="CodeEdit"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
text = "~ title_thing
|
||||
|
||||
if this = \"that\" or 'this'
|
||||
Nathan: Something
|
||||
- Then [if test.thing() == 2.0] => somewhere
|
||||
- Other => END!
|
||||
|
||||
~ somewhere
|
||||
|
||||
set has_something = true
|
||||
=> END"
|
||||
highlight_all_occurrences = true
|
||||
highlight_current_line = true
|
||||
draw_tabs = true
|
||||
syntax_highlighter = SubResource("SyntaxHighlighter_cobxx")
|
||||
scroll_past_end_of_file = true
|
||||
minimap_draw = true
|
||||
symbol_lookup_on_click = true
|
||||
line_folding = true
|
||||
gutters_draw_line_numbers = true
|
||||
gutters_draw_fold_gutter = true
|
||||
delimiter_strings = Array[String](["\" \""])
|
||||
delimiter_comments = Array[String](["#"])
|
||||
code_completion_enabled = true
|
||||
code_completion_prefixes = Array[String]([">", "<"])
|
||||
indent_automatic = true
|
||||
auto_brace_completion_enabled = true
|
||||
auto_brace_completion_highlight_matching = true
|
||||
auto_brace_completion_pairs = {
|
||||
"\"": "\"",
|
||||
"(": ")",
|
||||
"[": "]",
|
||||
"{": "}"
|
||||
}
|
||||
script = ExtResource("1_g324i")
|
||||
|
||||
[connection signal="caret_changed" from="." to="." method="_on_code_edit_caret_changed"]
|
||||
[connection signal="gutter_clicked" from="." to="." method="_on_code_edit_gutter_clicked"]
|
||||
[connection signal="symbol_lookup" from="." to="." method="_on_code_edit_symbol_lookup"]
|
||||
[connection signal="symbol_validate" from="." to="." method="_on_code_edit_symbol_validate"]
|
||||
[connection signal="text_changed" from="." to="." method="_on_code_edit_text_changed"]
|
||||
[connection signal="text_set" from="." to="." method="_on_code_edit_text_set"]
|
231
addons/dialogue_manager/components/code_edit_syntax_highlighter.gd
vendored
Normal file
231
addons/dialogue_manager/components/code_edit_syntax_highlighter.gd
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
@tool
|
||||
class_name DMSyntaxHighlighter extends SyntaxHighlighter
|
||||
|
||||
|
||||
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
||||
var compilation: DMCompilation = DMCompilation.new()
|
||||
var expression_parser = DMExpressionParser.new()
|
||||
|
||||
var cache: Dictionary = {}
|
||||
|
||||
|
||||
func _clear_highlighting_cache() -> void:
|
||||
cache.clear()
|
||||
|
||||
|
||||
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||
expression_parser.include_comments = true
|
||||
|
||||
var colors: Dictionary = {}
|
||||
var text_edit: TextEdit = get_text_edit()
|
||||
var text: String = text_edit.get_line(line)
|
||||
|
||||
# Prevent an error from popping up while developing
|
||||
if not is_instance_valid(text_edit) or text_edit.theme_overrides.is_empty():
|
||||
return colors
|
||||
|
||||
# Disable this, as well as the line at the bottom of this function to remove the cache.
|
||||
if text in cache:
|
||||
return cache[text]
|
||||
|
||||
var theme: Dictionary = text_edit.theme_overrides
|
||||
|
||||
var index: int = 0
|
||||
|
||||
match DMCompiler.get_line_type(text):
|
||||
DMConstants.TYPE_USING:
|
||||
colors[index] = { color = theme.conditions_color }
|
||||
colors[index + "using ".length()] = { color = theme.text_color }
|
||||
|
||||
DMConstants.TYPE_IMPORT:
|
||||
colors[index] = { color = theme.conditions_color }
|
||||
var import: RegExMatch = regex.IMPORT_REGEX.search(text)
|
||||
if import:
|
||||
colors[index + import.get_start("path") - 1] = { color = theme.strings_color }
|
||||
colors[index + import.get_end("path") + 1] = { color = theme.conditions_color }
|
||||
colors[index + import.get_start("prefix")] = { color = theme.text_color }
|
||||
|
||||
DMConstants.TYPE_COMMENT:
|
||||
colors[index] = { color = theme.comments_color }
|
||||
|
||||
DMConstants.TYPE_TITLE:
|
||||
colors[index] = { color = theme.titles_color }
|
||||
|
||||
DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN:
|
||||
colors[0] = { color = theme.conditions_color }
|
||||
index = text.find(" ")
|
||||
if index > -1:
|
||||
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0)
|
||||
if expression.size() == 0:
|
||||
colors[index] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index)
|
||||
|
||||
DMConstants.TYPE_MUTATION:
|
||||
colors[0] = { color = theme.mutations_color }
|
||||
index = text.find(" ")
|
||||
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0)
|
||||
if expression.size() == 0:
|
||||
colors[index] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index)
|
||||
|
||||
DMConstants.TYPE_GOTO:
|
||||
if text.strip_edges().begins_with("%"):
|
||||
colors[index] = { color = theme.symbols_color }
|
||||
index = text.find(" ")
|
||||
_highlight_goto(text, colors, index)
|
||||
|
||||
DMConstants.TYPE_RANDOM:
|
||||
colors[index] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE:
|
||||
if text.strip_edges().begins_with("%"):
|
||||
colors[index] = { color = theme.symbols_color }
|
||||
index = text.find(" ", text.find("%"))
|
||||
colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) }
|
||||
|
||||
var dialogue_text: String = text.substr(index, text.find("=>"))
|
||||
|
||||
# Highlight character name (but ignore ":" within line ID reference)
|
||||
var split_index: int = dialogue_text.replace("\\:", "??").find(":")
|
||||
if text.substr(split_index - 3, 3) != "[ID":
|
||||
colors[index + split_index + 1] = { color = theme.text_color }
|
||||
else:
|
||||
# If there's no character name then just highlight the text as dialogue.
|
||||
colors[index] = { color = theme.text_color }
|
||||
|
||||
# Interpolation
|
||||
var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text)
|
||||
for replacement: RegExMatch in replacements:
|
||||
var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2)
|
||||
var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start())
|
||||
var expression_index: int = index + replacement.get_start()
|
||||
colors[expression_index] = { color = theme.symbols_color }
|
||||
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
|
||||
colors[expression_index] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index + 2)
|
||||
colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color }
|
||||
colors[expression_index + expression_text.length() + 4] = { color = theme.text_color }
|
||||
# Tags (and inline mutations)
|
||||
var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("")
|
||||
var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true)
|
||||
for bbcode: Dictionary in bbcodes:
|
||||
var tag: String = bbcode.code
|
||||
var code: String = bbcode.raw_args
|
||||
if code.begins_with("["):
|
||||
colors[index + bbcode.start] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.start + 2] = { color = theme.text_color }
|
||||
var pipe_cursor: int = code.find("|")
|
||||
while pipe_cursor > -1:
|
||||
colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color }
|
||||
pipe_cursor = code.find("|", pipe_cursor + 1)
|
||||
colors[index + bbcode.end - 1] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.end + 1] = { color = theme.text_color }
|
||||
else:
|
||||
colors[index + bbcode.start] = { color = theme.symbols_color }
|
||||
if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"):
|
||||
if tag.begins_with("if"):
|
||||
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
|
||||
else:
|
||||
colors[index + bbcode.start + 1] = { color = theme.mutations_color }
|
||||
var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length())
|
||||
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
|
||||
colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color }
|
||||
else:
|
||||
_highlight_expression(expression, colors, index + 2)
|
||||
# else and closing if have no expression
|
||||
elif tag.begins_with("else") or tag.begins_with("/if"):
|
||||
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
|
||||
colors[index + bbcode.end] = { color = theme.symbols_color }
|
||||
colors[index + bbcode.end + 1] = { color = theme.text_color }
|
||||
# Jumps
|
||||
if "=> " in text or "=>< " in text:
|
||||
_highlight_goto(text, colors, index)
|
||||
|
||||
# Order the dictionary keys to prevent CodeEdit from having issues
|
||||
var ordered_colors: Dictionary = {}
|
||||
var ordered_keys: Array = colors.keys()
|
||||
ordered_keys.sort()
|
||||
for key_index: int in ordered_keys:
|
||||
ordered_colors[key_index] = colors[key_index]
|
||||
|
||||
cache[text] = ordered_colors
|
||||
return ordered_colors
|
||||
|
||||
|
||||
func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int:
|
||||
var theme: Dictionary = get_text_edit().theme_overrides
|
||||
var last_index: int = index
|
||||
for token: Dictionary in tokens:
|
||||
last_index = token.i
|
||||
match token.type:
|
||||
DMConstants.TOKEN_COMMENT:
|
||||
colors[index + token.i] = { color = theme.comments_color }
|
||||
|
||||
DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR:
|
||||
colors[index + token.i] = { color = theme.conditions_color }
|
||||
|
||||
DMConstants.TOKEN_VARIABLE:
|
||||
if token.value in ["true", "false"]:
|
||||
colors[index + token.i] = { color = theme.conditions_color }
|
||||
else:
|
||||
colors[index + token.i] = { color = theme.members_color }
|
||||
|
||||
DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \
|
||||
DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \
|
||||
DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TOKEN_STRING:
|
||||
colors[index + token.i] = { color = theme.strings_color }
|
||||
|
||||
DMConstants.TOKEN_FUNCTION:
|
||||
colors[index + token.i] = { color = theme.mutations_color }
|
||||
colors[index + token.i + token.function.length()] = { color = theme.symbols_color }
|
||||
for parameter: Array in token.value:
|
||||
last_index = _highlight_expression(parameter, colors, index)
|
||||
DMConstants.TOKEN_PARENS_CLOSE:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
||||
colors[index + token.i] = { color = theme.members_color }
|
||||
colors[index + token.i + token.variable.length()] = { color = theme.symbols_color }
|
||||
last_index = _highlight_expression(token.value, colors, index)
|
||||
DMConstants.TOKEN_ARRAY:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
for item: Array in token.value:
|
||||
last_index = _highlight_expression(item, colors, index)
|
||||
DMConstants.TOKEN_BRACKET_CLOSE:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
|
||||
DMConstants.TOKEN_DICTIONARY:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index)
|
||||
DMConstants.TOKEN_BRACE_CLOSE:
|
||||
colors[index + token.i] = { color = theme.symbols_color }
|
||||
last_index += 1
|
||||
|
||||
DMConstants.TOKEN_GROUP:
|
||||
last_index = _highlight_expression(token.value, colors, index)
|
||||
|
||||
return last_index
|
||||
|
||||
|
||||
func _highlight_goto(text: String, colors: Dictionary, index: int) -> int:
|
||||
var theme: Dictionary = get_text_edit().theme_overrides
|
||||
var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {})
|
||||
colors[goto_data.index] = { color = theme.jumps_color }
|
||||
if "{{" in text:
|
||||
index = text.find("{{", goto_data.index)
|
||||
var last_index: int = 0
|
||||
if goto_data.error:
|
||||
colors[index + 2] = { color = theme.critical_color }
|
||||
else:
|
||||
last_index = _highlight_expression(goto_data.expression, colors, index)
|
||||
index = text.find("}}", index + last_index)
|
||||
colors[index] = { color = theme.jumps_color }
|
||||
|
||||
return index
|
@@ -0,0 +1 @@
|
||||
uid://klpiq4tk3t7a
|
84
addons/dialogue_manager/components/download_update_panel.gd
vendored
Normal file
84
addons/dialogue_manager/components/download_update_panel.gd
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
@tool
|
||||
extends Control
|
||||
|
||||
|
||||
signal failed()
|
||||
signal updated(updated_to_version: String)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
const TEMP_FILE_NAME = "user://temp.zip"
|
||||
|
||||
|
||||
@onready var logo: TextureRect = %Logo
|
||||
@onready var label: Label = $VBox/Label
|
||||
@onready var http_request: HTTPRequest = $HTTPRequest
|
||||
@onready var download_button: Button = %DownloadButton
|
||||
|
||||
var next_version_release: Dictionary:
|
||||
set(value):
|
||||
next_version_release = value
|
||||
label.text = DialogueConstants.translate(&"update.is_available_for_download") % value.tag_name.substr(1)
|
||||
get:
|
||||
return next_version_release
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
$VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update")
|
||||
$VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes")
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_download_button_pressed() -> void:
|
||||
# Safeguard the actual dialogue manager repo from accidentally updating itself
|
||||
if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"):
|
||||
prints("You can't update the addon from within itself.")
|
||||
failed.emit()
|
||||
return
|
||||
|
||||
http_request.request(next_version_release.zipball_url)
|
||||
download_button.disabled = true
|
||||
download_button.text = DialogueConstants.translate(&"update.downloading")
|
||||
|
||||
|
||||
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
failed.emit()
|
||||
return
|
||||
|
||||
# Save the downloaded zip
|
||||
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
|
||||
zip_file.store_buffer(body)
|
||||
zip_file.close()
|
||||
|
||||
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager"))
|
||||
|
||||
var zip_reader: ZIPReader = ZIPReader.new()
|
||||
zip_reader.open(TEMP_FILE_NAME)
|
||||
var files: PackedStringArray = zip_reader.get_files()
|
||||
|
||||
var base_path = files[1]
|
||||
# Remove archive folder
|
||||
files.remove_at(0)
|
||||
# Remove assets folder
|
||||
files.remove_at(0)
|
||||
|
||||
for path in files:
|
||||
var new_file_path: String = path.replace(base_path, "")
|
||||
if path.ends_with("/"):
|
||||
DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path)
|
||||
else:
|
||||
var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE)
|
||||
file.store_buffer(zip_reader.read_file(path))
|
||||
|
||||
zip_reader.close()
|
||||
DirAccess.remove_absolute(TEMP_FILE_NAME)
|
||||
|
||||
updated.emit(next_version_release.tag_name.substr(1))
|
||||
|
||||
|
||||
func _on_notes_button_pressed() -> void:
|
||||
OS.shell_open(next_version_release.html_url)
|
@@ -0,0 +1 @@
|
||||
uid://kpwo418lb2t2
|
@@ -0,0 +1,60 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://kpwo418lb2t2" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"]
|
||||
[ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"]
|
||||
|
||||
[node name="DownloadUpdatePanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_4tm1k")
|
||||
|
||||
[node name="HTTPRequest" type="HTTPRequest" parent="."]
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -1.0
|
||||
offset_top = 9.0
|
||||
offset_right = -1.0
|
||||
offset_bottom = 9.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Logo" type="TextureRect" parent="VBox"]
|
||||
unique_name_in_owner = true
|
||||
clip_contents = true
|
||||
custom_minimum_size = Vector2(300, 80)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("2_4o2m6")
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="Label" type="Label" parent="VBox"]
|
||||
layout_mode = 2
|
||||
text = "v1.2.3 is available for download."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Center" type="CenterContainer" parent="VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DownloadButton" type="Button" parent="VBox/Center"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Download update"
|
||||
|
||||
[node name="Center2" type="CenterContainer" parent="VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="NotesButton" type="LinkButton" parent="VBox/Center2"]
|
||||
layout_mode = 2
|
||||
text = "Read release notes"
|
||||
|
||||
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
|
||||
[connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"]
|
||||
[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"]
|
48
addons/dialogue_manager/components/editor_property/editor_property.gd
vendored
Normal file
48
addons/dialogue_manager/components/editor_property/editor_property.gd
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
@tool
|
||||
extends EditorProperty
|
||||
|
||||
|
||||
const DialoguePropertyEditorControl = preload("./editor_property_control.tscn")
|
||||
|
||||
|
||||
var editor_plugin: EditorPlugin
|
||||
|
||||
var control = DialoguePropertyEditorControl.instantiate()
|
||||
var current_value: Resource
|
||||
var is_updating: bool = false
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
add_child(control)
|
||||
|
||||
control.resource = current_value
|
||||
|
||||
control.pressed.connect(_on_button_pressed)
|
||||
control.resource_changed.connect(_on_resource_changed)
|
||||
|
||||
|
||||
func _update_property() -> void:
|
||||
var next_value = get_edited_object()[get_edited_property()]
|
||||
|
||||
# The resource might have been deleted elsewhere so check that it's not in a weird state
|
||||
if is_instance_valid(next_value) and not next_value.resource_path.ends_with(".dialogue"):
|
||||
emit_changed(get_edited_property(), null)
|
||||
return
|
||||
|
||||
if next_value == current_value: return
|
||||
|
||||
is_updating = true
|
||||
current_value = next_value
|
||||
control.resource = current_value
|
||||
is_updating = false
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_button_pressed() -> void:
|
||||
editor_plugin.edit(current_value)
|
||||
|
||||
|
||||
func _on_resource_changed(next_resource: Resource) -> void:
|
||||
emit_changed(get_edited_property(), next_resource)
|
@@ -0,0 +1 @@
|
||||
uid://nyypeje1a036
|
147
addons/dialogue_manager/components/editor_property/editor_property_control.gd
vendored
Normal file
147
addons/dialogue_manager/components/editor_property/editor_property_control.gd
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
@tool
|
||||
extends HBoxContainer
|
||||
|
||||
|
||||
signal pressed()
|
||||
signal resource_changed(next_resource: Resource)
|
||||
|
||||
|
||||
const ITEM_NEW = 100
|
||||
const ITEM_QUICK_LOAD = 200
|
||||
const ITEM_LOAD = 201
|
||||
const ITEM_EDIT = 300
|
||||
const ITEM_CLEAR = 301
|
||||
const ITEM_FILESYSTEM = 400
|
||||
|
||||
|
||||
@onready var button: Button = $ResourceButton
|
||||
@onready var menu_button: Button = $MenuButton
|
||||
@onready var menu: PopupMenu = $Menu
|
||||
@onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog
|
||||
@onready var files_list = $QuickOpenDialog/FilesList
|
||||
@onready var new_dialog: FileDialog = $NewDialog
|
||||
@onready var open_dialog: FileDialog = $OpenDialog
|
||||
|
||||
var editor_plugin: EditorPlugin
|
||||
|
||||
var resource: Resource:
|
||||
set(next_resource):
|
||||
resource = next_resource
|
||||
if button:
|
||||
button.resource = resource
|
||||
get:
|
||||
return resource
|
||||
|
||||
var is_waiting_for_file: bool = false
|
||||
var quick_selected_file: String = ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
menu_button.icon = get_theme_icon("GuiDropdown", "EditorIcons")
|
||||
editor_plugin = Engine.get_meta("DialogueManagerPlugin")
|
||||
|
||||
|
||||
func build_menu() -> void:
|
||||
menu.clear()
|
||||
|
||||
menu.add_icon_item(editor_plugin._get_plugin_icon(), "New Dialogue", ITEM_NEW)
|
||||
menu.add_separator()
|
||||
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Quick Load", ITEM_QUICK_LOAD)
|
||||
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Load", ITEM_LOAD)
|
||||
if resource:
|
||||
menu.add_icon_item(get_theme_icon("Edit", "EditorIcons"), "Edit", ITEM_EDIT)
|
||||
menu.add_icon_item(get_theme_icon("Clear", "EditorIcons"), "Clear", ITEM_CLEAR)
|
||||
menu.add_separator()
|
||||
menu.add_item("Show in FileSystem", ITEM_FILESYSTEM)
|
||||
|
||||
menu.size = Vector2.ZERO
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_new_dialog_file_selected(path: String) -> void:
|
||||
editor_plugin.main_view.new_file(path)
|
||||
is_waiting_for_file = false
|
||||
if Engine.get_meta("DMCache").has_file(path):
|
||||
resource_changed.emit(load(path))
|
||||
else:
|
||||
var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource
|
||||
next_resource.resource_path = path
|
||||
resource_changed.emit(next_resource)
|
||||
|
||||
|
||||
func _on_open_dialog_file_selected(file: String) -> void:
|
||||
resource_changed.emit(load(file))
|
||||
|
||||
|
||||
func _on_file_dialog_canceled() -> void:
|
||||
is_waiting_for_file = false
|
||||
|
||||
|
||||
func _on_resource_button_pressed() -> void:
|
||||
if is_instance_valid(resource):
|
||||
EditorInterface.call_deferred("edit_resource", resource)
|
||||
else:
|
||||
build_menu()
|
||||
menu.position = get_viewport().position + Vector2i(
|
||||
button.global_position.x + button.size.x - menu.size.x,
|
||||
2 + menu_button.global_position.y + button.size.y
|
||||
)
|
||||
menu.popup()
|
||||
|
||||
|
||||
func _on_resource_button_resource_dropped(next_resource: Resource) -> void:
|
||||
resource_changed.emit(next_resource)
|
||||
|
||||
|
||||
func _on_menu_button_pressed() -> void:
|
||||
build_menu()
|
||||
menu.position = get_viewport().position + Vector2i(
|
||||
menu_button.global_position.x + menu_button.size.x - menu.size.x,
|
||||
2 + menu_button.global_position.y + menu_button.size.y
|
||||
)
|
||||
menu.popup()
|
||||
|
||||
|
||||
func _on_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
ITEM_NEW:
|
||||
is_waiting_for_file = true
|
||||
new_dialog.popup_centered()
|
||||
|
||||
ITEM_QUICK_LOAD:
|
||||
quick_selected_file = ""
|
||||
files_list.files = Engine.get_meta("DMCache").get_files()
|
||||
if resource:
|
||||
files_list.select_file(resource.resource_path)
|
||||
quick_open_dialog.popup_centered()
|
||||
files_list.focus_filter()
|
||||
|
||||
ITEM_LOAD:
|
||||
is_waiting_for_file = true
|
||||
open_dialog.popup_centered()
|
||||
|
||||
ITEM_EDIT:
|
||||
EditorInterface.call_deferred("edit_resource", resource)
|
||||
|
||||
ITEM_CLEAR:
|
||||
resource_changed.emit(null)
|
||||
|
||||
ITEM_FILESYSTEM:
|
||||
var file_system = EditorInterface.get_file_system_dock()
|
||||
file_system.navigate_to_path(resource.resource_path)
|
||||
|
||||
|
||||
func _on_files_list_file_double_clicked(file_path: String) -> void:
|
||||
resource_changed.emit(load(file_path))
|
||||
quick_open_dialog.hide()
|
||||
|
||||
|
||||
func _on_files_list_file_selected(file_path: String) -> void:
|
||||
quick_selected_file = file_path
|
||||
|
||||
|
||||
func _on_quick_open_dialog_confirmed() -> void:
|
||||
if quick_selected_file != "":
|
||||
resource_changed.emit(load(quick_selected_file))
|
@@ -0,0 +1 @@
|
||||
uid://dooe2pflnqtve
|
@@ -0,0 +1,58 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dooe2pflnqtve" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"]
|
||||
[ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"]
|
||||
[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"]
|
||||
|
||||
[node name="PropertyEditorButton" type="HBoxContainer"]
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 0
|
||||
script = ExtResource("1_het12")
|
||||
|
||||
[node name="ResourceButton" parent="." instance=ExtResource("2_hh3d4")]
|
||||
layout_mode = 2
|
||||
text = "<empty>"
|
||||
text_overrun_behavior = 3
|
||||
clip_text = true
|
||||
|
||||
[node name="MenuButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Menu" type="PopupMenu" parent="."]
|
||||
|
||||
[node name="QuickOpenDialog" type="ConfirmationDialog" parent="."]
|
||||
title = "Find Dialogue Resource"
|
||||
size = Vector2i(400, 600)
|
||||
min_size = Vector2i(400, 600)
|
||||
ok_button_text = "Open"
|
||||
|
||||
[node name="FilesList" parent="QuickOpenDialog" instance=ExtResource("3_l8fp6")]
|
||||
|
||||
[node name="NewDialog" type="FileDialog" parent="."]
|
||||
size = Vector2i(900, 750)
|
||||
min_size = Vector2i(900, 750)
|
||||
dialog_hide_on_ok = true
|
||||
filters = PackedStringArray("*.dialogue ; Dialogue")
|
||||
|
||||
[node name="OpenDialog" type="FileDialog" parent="."]
|
||||
title = "Open a File"
|
||||
size = Vector2i(900, 750)
|
||||
min_size = Vector2i(900, 750)
|
||||
ok_button_text = "Open"
|
||||
dialog_hide_on_ok = true
|
||||
file_mode = 0
|
||||
filters = PackedStringArray("*.dialogue ; Dialogue")
|
||||
|
||||
[connection signal="pressed" from="ResourceButton" to="." method="_on_resource_button_pressed"]
|
||||
[connection signal="resource_dropped" from="ResourceButton" to="." method="_on_resource_button_resource_dropped"]
|
||||
[connection signal="pressed" from="MenuButton" to="." method="_on_menu_button_pressed"]
|
||||
[connection signal="id_pressed" from="Menu" to="." method="_on_menu_id_pressed"]
|
||||
[connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"]
|
||||
[connection signal="file_double_clicked" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_double_clicked"]
|
||||
[connection signal="file_selected" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_selected"]
|
||||
[connection signal="canceled" from="NewDialog" to="." method="_on_file_dialog_canceled"]
|
||||
[connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"]
|
||||
[connection signal="canceled" from="OpenDialog" to="." method="_on_file_dialog_canceled"]
|
||||
[connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"]
|
48
addons/dialogue_manager/components/editor_property/resource_button.gd
vendored
Normal file
48
addons/dialogue_manager/components/editor_property/resource_button.gd
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
@tool
|
||||
extends Button
|
||||
|
||||
|
||||
signal resource_dropped(next_resource: Resource)
|
||||
|
||||
|
||||
var resource: Resource:
|
||||
set(next_resource):
|
||||
resource = next_resource
|
||||
if resource:
|
||||
icon = Engine.get_meta("DialogueManagerPlugin")._get_plugin_icon()
|
||||
text = resource.resource_path.get_file().replace(".dialogue", "")
|
||||
else:
|
||||
icon = null
|
||||
text = "<empty>"
|
||||
get:
|
||||
return resource
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
match what:
|
||||
NOTIFICATION_DRAG_BEGIN:
|
||||
var data = get_viewport().gui_get_drag_data()
|
||||
if typeof(data) == TYPE_DICTIONARY and data.type == "files" and data.files.size() > 0 and data.files[0].ends_with(".dialogue"):
|
||||
add_theme_stylebox_override("normal", get_theme_stylebox("focus", "LineEdit"))
|
||||
add_theme_stylebox_override("hover", get_theme_stylebox("focus", "LineEdit"))
|
||||
|
||||
NOTIFICATION_DRAG_END:
|
||||
self.resource = resource
|
||||
remove_theme_stylebox_override("normal")
|
||||
remove_theme_stylebox_override("hover")
|
||||
|
||||
|
||||
func _can_drop_data(at_position: Vector2, data) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY: return false
|
||||
if data.type != "files": return false
|
||||
|
||||
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
|
||||
return files.size() > 0
|
||||
|
||||
|
||||
func _drop_data(at_position: Vector2, data) -> void:
|
||||
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
|
||||
|
||||
if files.size() == 0: return
|
||||
|
||||
resource_dropped.emit(load(files[0]))
|
@@ -0,0 +1 @@
|
||||
uid://damhqta55t67c
|
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://damhqta55t67c" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"]
|
||||
|
||||
[node name="ResourceButton" type="Button"]
|
||||
offset_right = 8.0
|
||||
offset_bottom = 8.0
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_7u2i7")
|
85
addons/dialogue_manager/components/errors_panel.gd
vendored
Normal file
85
addons/dialogue_manager/components/errors_panel.gd
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
@tool
|
||||
extends HBoxContainer
|
||||
|
||||
|
||||
signal error_pressed(line_number)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
|
||||
@onready var error_button: Button = $ErrorButton
|
||||
@onready var next_button: Button = $NextButton
|
||||
@onready var count_label: Label = $CountLabel
|
||||
@onready var previous_button: Button = $PreviousButton
|
||||
|
||||
## The index of the current error being shown
|
||||
var error_index: int = 0:
|
||||
set(next_error_index):
|
||||
error_index = wrap(next_error_index, 0, errors.size())
|
||||
show_error()
|
||||
get:
|
||||
return error_index
|
||||
|
||||
## The list of all errors
|
||||
var errors: Array = []:
|
||||
set(next_errors):
|
||||
errors = next_errors
|
||||
self.error_index = 0
|
||||
get:
|
||||
return errors
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
hide()
|
||||
|
||||
|
||||
## Set up colors and icons
|
||||
func apply_theme() -> void:
|
||||
error_button.add_theme_color_override("font_color", get_theme_color("error_color", "Editor"))
|
||||
error_button.add_theme_color_override("font_hover_color", get_theme_color("error_color", "Editor"))
|
||||
error_button.icon = get_theme_icon("StatusError", "EditorIcons")
|
||||
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
|
||||
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
|
||||
|
||||
|
||||
## Move the error index to match a given line
|
||||
func show_error_for_line_number(line_number: int) -> void:
|
||||
for i in range(0, errors.size()):
|
||||
if errors[i].line_number == line_number:
|
||||
self.error_index = i
|
||||
|
||||
|
||||
## Show the current error
|
||||
func show_error() -> void:
|
||||
if errors.size() == 0:
|
||||
hide()
|
||||
else:
|
||||
show()
|
||||
count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() })
|
||||
var error = errors[error_index]
|
||||
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
|
||||
if error.has("external_error"):
|
||||
error_button.text += " " + DialogueConstants.get_error_message(error.external_error)
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_errors_panel_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_error_button_pressed() -> void:
|
||||
error_pressed.emit(errors[error_index].line_number, errors[error_index].column_number)
|
||||
|
||||
|
||||
func _on_previous_button_pressed() -> void:
|
||||
self.error_index -= 1
|
||||
_on_error_button_pressed()
|
||||
|
||||
|
||||
func _on_next_button_pressed() -> void:
|
||||
self.error_index += 1
|
||||
_on_error_button_pressed()
|
1
addons/dialogue_manager/components/errors_panel.gd.uid
Normal file
1
addons/dialogue_manager/components/errors_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d2l8nlb6hhrfp
|
56
addons/dialogue_manager/components/errors_panel.tscn
Normal file
56
addons/dialogue_manager/components/errors_panel.tscn
Normal file
@@ -0,0 +1,56 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"]
|
||||
|
||||
[sub_resource type="Image" id="Image_w0gko"]
|
||||
data = {
|
||||
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"]
|
||||
image = SubResource("Image_w0gko")
|
||||
|
||||
[node name="ErrorsPanel" type="HBoxContainer"]
|
||||
visible = false
|
||||
offset_right = 1024.0
|
||||
offset_bottom = 600.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_nfm3c")
|
||||
metadata/_edit_layout_mode = 1
|
||||
|
||||
[node name="ErrorButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
|
||||
theme_override_constants/h_separation = 3
|
||||
icon = SubResource("ImageTexture_s6fxl")
|
||||
flat = true
|
||||
alignment = 0
|
||||
text_overrun_behavior = 4
|
||||
|
||||
[node name="Spacer" type="Control" parent="."]
|
||||
custom_minimum_size = Vector2(40, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PreviousButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
icon = SubResource("ImageTexture_s6fxl")
|
||||
flat = true
|
||||
|
||||
[node name="CountLabel" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="NextButton" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
icon = SubResource("ImageTexture_s6fxl")
|
||||
flat = true
|
||||
|
||||
[connection signal="pressed" from="ErrorButton" to="." method="_on_error_button_pressed"]
|
||||
[connection signal="pressed" from="PreviousButton" to="." method="_on_previous_button_pressed"]
|
||||
[connection signal="pressed" from="NextButton" to="." method="_on_next_button_pressed"]
|
150
addons/dialogue_manager/components/files_list.gd
vendored
Normal file
150
addons/dialogue_manager/components/files_list.gd
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
|
||||
signal file_selected(file_path: String)
|
||||
signal file_popup_menu_requested(at_position: Vector2)
|
||||
signal file_double_clicked(file_path: String)
|
||||
signal file_middle_clicked(file_path: String)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
const MODIFIED_SUFFIX = "(*)"
|
||||
|
||||
|
||||
@export var icon: Texture2D
|
||||
|
||||
@onready var filter_edit: LineEdit = $FilterEdit
|
||||
@onready var list: ItemList = $List
|
||||
|
||||
var file_map: Dictionary = {}
|
||||
|
||||
var current_file_path: String = ""
|
||||
var last_selected_file_path: String = ""
|
||||
|
||||
var files: PackedStringArray = []:
|
||||
set(next_files):
|
||||
files = next_files
|
||||
files.sort()
|
||||
update_file_map()
|
||||
apply_filter()
|
||||
get:
|
||||
return files
|
||||
|
||||
var unsaved_files: Array[String] = []
|
||||
|
||||
var filter: String = "":
|
||||
set(next_filter):
|
||||
filter = next_filter
|
||||
apply_filter()
|
||||
get:
|
||||
return filter
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
|
||||
filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter")
|
||||
|
||||
|
||||
func focus_filter() -> void:
|
||||
filter_edit.grab_focus()
|
||||
|
||||
|
||||
func select_file(file: String) -> void:
|
||||
list.deselect_all()
|
||||
for i in range(0, list.get_item_count()):
|
||||
var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "")
|
||||
if item_text == get_nice_file(file, item_text.count("/") + 1):
|
||||
list.select(i)
|
||||
last_selected_file_path = file
|
||||
|
||||
|
||||
func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void:
|
||||
if not file in unsaved_files and is_unsaved:
|
||||
unsaved_files.append(file)
|
||||
elif file in unsaved_files and not is_unsaved:
|
||||
unsaved_files.erase(file)
|
||||
apply_filter()
|
||||
|
||||
|
||||
func update_file_map() -> void:
|
||||
file_map = {}
|
||||
for file in files:
|
||||
var nice_file: String = get_nice_file(file)
|
||||
|
||||
# See if a value with just the file name is already in the map
|
||||
for key in file_map.keys():
|
||||
if file_map[key] == nice_file:
|
||||
var bit_count = nice_file.count("/") + 2
|
||||
|
||||
var existing_nice_file = get_nice_file(key, bit_count)
|
||||
nice_file = get_nice_file(file, bit_count)
|
||||
|
||||
while nice_file == existing_nice_file:
|
||||
bit_count += 1
|
||||
existing_nice_file = get_nice_file(key, bit_count)
|
||||
nice_file = get_nice_file(file, bit_count)
|
||||
|
||||
file_map[key] = existing_nice_file
|
||||
|
||||
file_map[file] = nice_file
|
||||
|
||||
|
||||
func get_nice_file(file_path: String, path_bit_count: int = 1) -> String:
|
||||
var bits = file_path.replace("res://", "").replace(".dialogue", "").split("/")
|
||||
bits = bits.slice(-path_bit_count)
|
||||
return "/".join(bits)
|
||||
|
||||
|
||||
func apply_filter() -> void:
|
||||
list.clear()
|
||||
for file in file_map.keys():
|
||||
if filter == "" or filter.to_lower() in file.to_lower():
|
||||
var nice_file = file_map[file]
|
||||
if file in unsaved_files:
|
||||
nice_file += MODIFIED_SUFFIX
|
||||
var new_id := list.add_item(nice_file)
|
||||
list.set_item_icon(new_id, icon)
|
||||
|
||||
select_file(current_file_path)
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
if is_instance_valid(filter_edit):
|
||||
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||
if is_instance_valid(list):
|
||||
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_filter_edit_text_changed(new_text: String) -> void:
|
||||
self.filter = new_text
|
||||
|
||||
|
||||
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
|
||||
var file = file_map.find_key(item_text)
|
||||
|
||||
if mouse_button_index == MOUSE_BUTTON_LEFT or mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||
select_file(file)
|
||||
file_selected.emit(file)
|
||||
if mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||
file_popup_menu_requested.emit(at_position)
|
||||
|
||||
if mouse_button_index == MOUSE_BUTTON_MIDDLE:
|
||||
file_middle_clicked.emit(file)
|
||||
|
||||
|
||||
func _on_list_item_activated(index: int) -> void:
|
||||
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
|
||||
var file = file_map.find_key(item_text)
|
||||
select_file(file)
|
||||
file_double_clicked.emit(file)
|
1
addons/dialogue_manager/components/files_list.gd.uid
Normal file
1
addons/dialogue_manager/components/files_list.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dqa4a4wwoo0aa
|
29
addons/dialogue_manager/components/files_list.tscn
Normal file
29
addons/dialogue_manager/components/files_list.tscn
Normal file
@@ -0,0 +1,29 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"]
|
||||
[ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"]
|
||||
|
||||
[node name="FilesList" type="VBoxContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_cytii")
|
||||
icon = ExtResource("2_3ijx1")
|
||||
|
||||
[node name="FilterEdit" type="LineEdit" parent="."]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Filter files"
|
||||
clear_button_enabled = true
|
||||
|
||||
[node name="List" type="ItemList" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
allow_rmb_select = true
|
||||
|
||||
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
|
||||
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
|
||||
[connection signal="item_activated" from="List" to="." method="_on_list_item_activated"]
|
||||
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]
|
229
addons/dialogue_manager/components/find_in_files.gd
vendored
Normal file
229
addons/dialogue_manager/components/find_in_files.gd
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
@tool
|
||||
extends Control
|
||||
|
||||
signal result_selected(path: String, cursor: Vector2, length: int)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
|
||||
@export var main_view: Control
|
||||
@export var code_edit: CodeEdit
|
||||
|
||||
@onready var input: LineEdit = %Input
|
||||
@onready var search_button: Button = %SearchButton
|
||||
@onready var match_case_button: CheckBox = %MatchCaseButton
|
||||
@onready var replace_toggle: CheckButton = %ReplaceToggle
|
||||
@onready var replace_container: VBoxContainer = %ReplaceContainer
|
||||
@onready var replace_input: LineEdit = %ReplaceInput
|
||||
@onready var replace_selected_button: Button = %ReplaceSelectedButton
|
||||
@onready var replace_all_button: Button = %ReplaceAllButton
|
||||
@onready var results_container: VBoxContainer = %ResultsContainer
|
||||
@onready var result_template: HBoxContainer = %ResultTemplate
|
||||
|
||||
var current_results: Dictionary = {}:
|
||||
set(value):
|
||||
current_results = value
|
||||
update_results_view()
|
||||
if current_results.size() == 0:
|
||||
replace_selected_button.disabled = true
|
||||
replace_all_button.disabled = true
|
||||
else:
|
||||
replace_selected_button.disabled = false
|
||||
replace_all_button.disabled = false
|
||||
get:
|
||||
return current_results
|
||||
|
||||
var selections: PackedStringArray = []
|
||||
|
||||
|
||||
func prepare() -> void:
|
||||
input.grab_focus()
|
||||
|
||||
var template_label = result_template.get_node("Label")
|
||||
template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color
|
||||
template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font"))
|
||||
|
||||
replace_toggle.set_pressed_no_signal(false)
|
||||
replace_container.hide()
|
||||
|
||||
$VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find")
|
||||
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
|
||||
input.text = ""
|
||||
search_button.text = DialogueConstants.translate(&"search.find_all")
|
||||
match_case_button.text = DialogueConstants.translate(&"search.match_case")
|
||||
replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace")
|
||||
$VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
|
||||
replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder")
|
||||
replace_input.text = ""
|
||||
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
|
||||
replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected")
|
||||
|
||||
selections.clear()
|
||||
self.current_results = {}
|
||||
|
||||
#region helpers
|
||||
|
||||
|
||||
func update_results_view() -> void:
|
||||
for child in results_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
for path in current_results.keys():
|
||||
var path_label: Label = Label.new()
|
||||
path_label.text = path
|
||||
# Show open files
|
||||
if main_view.open_buffers.has(path):
|
||||
path_label.text += "(*)"
|
||||
results_container.add_child(path_label)
|
||||
for path_result in current_results.get(path):
|
||||
var result_item: HBoxContainer = result_template.duplicate()
|
||||
|
||||
var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox
|
||||
var key: String = get_selection_key(path, path_result)
|
||||
checkbox.toggled.connect(func(is_pressed):
|
||||
if is_pressed:
|
||||
if not selections.has(key):
|
||||
selections.append(key)
|
||||
else:
|
||||
if selections.has(key):
|
||||
selections.remove_at(selections.find(key))
|
||||
)
|
||||
checkbox.set_pressed_no_signal(selections.has(key))
|
||||
checkbox.visible = replace_toggle.button_pressed
|
||||
|
||||
var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel
|
||||
var colors: Dictionary = code_edit.theme_overrides
|
||||
var highlight: String = ""
|
||||
if replace_toggle.button_pressed:
|
||||
var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
|
||||
highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]"
|
||||
else:
|
||||
highlight = "[bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
|
||||
var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length())
|
||||
result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text]
|
||||
result_label.gui_input.connect(func(event):
|
||||
if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click:
|
||||
result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length())
|
||||
)
|
||||
|
||||
results_container.add_child(result_item)
|
||||
|
||||
|
||||
func find_in_files() -> Dictionary:
|
||||
var results: Dictionary = {}
|
||||
|
||||
var q: String = input.text
|
||||
var cache = Engine.get_meta("DMCache")
|
||||
var file: FileAccess
|
||||
for path in cache.get_files():
|
||||
var path_results: Array = []
|
||||
var lines: PackedStringArray = []
|
||||
|
||||
if main_view.open_buffers.has(path):
|
||||
lines = main_view.open_buffers.get(path).text.split("\n")
|
||||
else:
|
||||
file = FileAccess.open(path, FileAccess.READ)
|
||||
lines = file.get_as_text().split("\n")
|
||||
|
||||
for i in range(0, lines.size()):
|
||||
var index: int = find_in_line(lines[i], q)
|
||||
while index > -1:
|
||||
path_results.append({
|
||||
line = i,
|
||||
index = index,
|
||||
text = lines[i],
|
||||
matched_text = lines[i].substr(index, q.length()),
|
||||
query = q
|
||||
})
|
||||
index = find_in_line(lines[i], q, index + q.length())
|
||||
|
||||
if file != null and file.is_open():
|
||||
file.close()
|
||||
|
||||
if path_results.size() > 0:
|
||||
results[path] = path_results
|
||||
|
||||
return results
|
||||
|
||||
|
||||
func get_selection_key(path: String, path_result: Dictionary) -> String:
|
||||
return "%s-%d-%d" % [path, path_result.line, path_result.index]
|
||||
|
||||
|
||||
func find_in_line(line: String, query: String, from_index: int = 0) -> int:
|
||||
if match_case_button.button_pressed:
|
||||
return line.find(query, from_index)
|
||||
else:
|
||||
return line.findn(query, from_index)
|
||||
|
||||
|
||||
func replace_results(only_selected: bool) -> void:
|
||||
var file: FileAccess
|
||||
var lines: PackedStringArray = []
|
||||
for path in current_results:
|
||||
if main_view.open_buffers.has(path):
|
||||
lines = main_view.open_buffers.get(path).text.split("\n")
|
||||
else:
|
||||
file = FileAccess.open(path, FileAccess.READ_WRITE)
|
||||
lines = file.get_as_text().split("\n")
|
||||
|
||||
# Read the results in reverse because we're going to be modifying them as we go
|
||||
var path_results: Array = current_results.get(path).duplicate()
|
||||
path_results.reverse()
|
||||
for path_result in path_results:
|
||||
var key: String = get_selection_key(path, path_result)
|
||||
if not only_selected or (only_selected and selections.has(key)):
|
||||
lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length())
|
||||
|
||||
var replaced_text: String = "\n".join(lines)
|
||||
if file != null and file.is_open():
|
||||
file.seek(0)
|
||||
file.store_string(replaced_text)
|
||||
file.close()
|
||||
else:
|
||||
main_view.open_buffers.get(path).text = replaced_text
|
||||
if main_view.current_file_path == path:
|
||||
code_edit.text = replaced_text
|
||||
|
||||
current_results = find_in_files()
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region signals
|
||||
|
||||
|
||||
func _on_search_button_pressed() -> void:
|
||||
selections.clear()
|
||||
self.current_results = find_in_files()
|
||||
|
||||
|
||||
func _on_input_text_submitted(new_text: String) -> void:
|
||||
_on_search_button_pressed()
|
||||
|
||||
|
||||
func _on_replace_toggle_toggled(toggled_on: bool) -> void:
|
||||
replace_container.visible = toggled_on
|
||||
if toggled_on:
|
||||
replace_input.grab_focus()
|
||||
update_results_view()
|
||||
|
||||
|
||||
func _on_replace_input_text_changed(new_text: String) -> void:
|
||||
update_results_view()
|
||||
|
||||
|
||||
func _on_replace_selected_button_pressed() -> void:
|
||||
replace_results(true)
|
||||
|
||||
|
||||
func _on_replace_all_button_pressed() -> void:
|
||||
replace_results(false)
|
||||
|
||||
|
||||
func _on_match_case_button_toggled(toggled_on: bool) -> void:
|
||||
_on_search_button_pressed()
|
||||
|
||||
|
||||
#endregion
|
1
addons/dialogue_manager/components/find_in_files.gd.uid
Normal file
1
addons/dialogue_manager/components/find_in_files.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://q368fmxxa8sd
|
139
addons/dialogue_manager/components/find_in_files.tscn
Normal file
139
addons/dialogue_manager/components/find_in_files.tscn
Normal file
@@ -0,0 +1,139 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://q368fmxxa8sd" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"]
|
||||
bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137)
|
||||
corner_detail = 1
|
||||
|
||||
[node name="FindInFiles" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_3xicy")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="FindContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/FindContainer"]
|
||||
layout_mode = 2
|
||||
text = "Find:"
|
||||
|
||||
[node name="Input" type="LineEdit" parent="VBoxContainer/HBoxContainer/FindContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
clear_button_enabled = true
|
||||
|
||||
[node name="FindToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/FindContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SearchButton" type="Button" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Find all..."
|
||||
|
||||
[node name="MatchCaseButton" type="CheckBox" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Match case"
|
||||
|
||||
[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ReplaceToggle" type="CheckButton" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Replace"
|
||||
|
||||
[node name="ReplaceContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ReplaceLabel" type="Label" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
|
||||
layout_mode = 2
|
||||
text = "Replace with:"
|
||||
|
||||
[node name="ReplaceInput" type="LineEdit" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
clear_button_enabled = true
|
||||
|
||||
[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ReplaceSelectedButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Replace selected"
|
||||
|
||||
[node name="ReplaceAllButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Replace all"
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
follow_focus = true
|
||||
|
||||
[node name="ResultsContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 0
|
||||
|
||||
[node name="ResultTemplate" type="HBoxContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 155.0
|
||||
offset_top = -74.0
|
||||
offset_right = 838.0
|
||||
offset_bottom = -51.0
|
||||
|
||||
[node name="CheckBox" type="CheckBox" parent="ResultTemplate"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="RichTextLabel" parent="ResultTemplate"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
theme_override_styles/focus = SubResource("StyleBoxFlat_owohg")
|
||||
bbcode_enabled = true
|
||||
text = "Result"
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
|
||||
[connection signal="text_submitted" from="VBoxContainer/HBoxContainer/FindContainer/Input" to="." method="_on_input_text_submitted"]
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/SearchButton" to="." method="_on_search_button_pressed"]
|
||||
[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/MatchCaseButton" to="." method="_on_match_case_button_toggled"]
|
||||
[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/ReplaceToggle" to="." method="_on_replace_toggle_toggled"]
|
||||
[connection signal="text_changed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceInput" to="." method="_on_replace_input_text_changed"]
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceSelectedButton" to="." method="_on_replace_selected_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]
|
218
addons/dialogue_manager/components/search_and_replace.gd
vendored
Normal file
218
addons/dialogue_manager/components/search_and_replace.gd
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
|
||||
signal open_requested()
|
||||
signal close_requested()
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
|
||||
@onready var input: LineEdit = $Search/Input
|
||||
@onready var result_label: Label = $Search/ResultLabel
|
||||
@onready var previous_button: Button = $Search/PreviousButton
|
||||
@onready var next_button: Button = $Search/NextButton
|
||||
@onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox
|
||||
@onready var replace_check_button: CheckButton = $Search/ReplaceCheckButton
|
||||
@onready var replace_panel: HBoxContainer = $Replace
|
||||
@onready var replace_input: LineEdit = $Replace/Input
|
||||
@onready var replace_button: Button = $Replace/ReplaceButton
|
||||
@onready var replace_all_button: Button = $Replace/ReplaceAllButton
|
||||
|
||||
# The code edit we will be affecting (for some reason exporting this didn't work)
|
||||
var code_edit: CodeEdit:
|
||||
set(next_code_edit):
|
||||
code_edit = next_code_edit
|
||||
code_edit.gui_input.connect(_on_text_edit_gui_input)
|
||||
code_edit.text_changed.connect(_on_text_edit_text_changed)
|
||||
get:
|
||||
return code_edit
|
||||
|
||||
var results: Array = []
|
||||
var result_index: int = -1:
|
||||
set(next_result_index):
|
||||
result_index = next_result_index
|
||||
if results.size() > 0:
|
||||
var r = results[result_index]
|
||||
code_edit.set_caret_line(r[0])
|
||||
code_edit.select(r[0], r[1], r[0], r[1] + r[2])
|
||||
else:
|
||||
result_index = -1
|
||||
if is_instance_valid(code_edit):
|
||||
code_edit.deselect()
|
||||
|
||||
result_label.text = DialogueConstants.translate(&"n_of_n").format({ index = result_index + 1, total = results.size() })
|
||||
get:
|
||||
return result_index
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
|
||||
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
|
||||
previous_button.tooltip_text = DialogueConstants.translate(&"search.previous")
|
||||
next_button.tooltip_text = DialogueConstants.translate(&"search.next")
|
||||
match_case_button.text = DialogueConstants.translate(&"search.match_case")
|
||||
$Search/ReplaceCheckButton.text = DialogueConstants.translate(&"search.toggle_replace")
|
||||
replace_button.text = DialogueConstants.translate(&"search.replace")
|
||||
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
|
||||
$Replace/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
|
||||
|
||||
self.result_index = -1
|
||||
|
||||
replace_panel.hide()
|
||||
replace_button.disabled = true
|
||||
replace_all_button.disabled = true
|
||||
|
||||
hide()
|
||||
|
||||
|
||||
func focus_line_edit() -> void:
|
||||
input.grab_focus()
|
||||
input.select_all()
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
if is_instance_valid(previous_button):
|
||||
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
|
||||
if is_instance_valid(next_button):
|
||||
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
|
||||
|
||||
|
||||
# Find text in the code
|
||||
func search(text: String = "", default_result_index: int = 0) -> void:
|
||||
results.clear()
|
||||
|
||||
if text == "":
|
||||
text = input.text
|
||||
|
||||
var lines = code_edit.text.split("\n")
|
||||
for line_number in range(0, lines.size()):
|
||||
var line = lines[line_number]
|
||||
|
||||
var column = find_in_line(line, text, 0)
|
||||
while column > -1:
|
||||
results.append([line_number, column, text.length()])
|
||||
column = find_in_line(line, text, column + 1)
|
||||
|
||||
if results.size() > 0:
|
||||
replace_button.disabled = false
|
||||
replace_all_button.disabled = false
|
||||
else:
|
||||
replace_button.disabled = true
|
||||
replace_all_button.disabled = true
|
||||
|
||||
self.result_index = clamp(default_result_index, 0, results.size() - 1)
|
||||
|
||||
|
||||
# Find text in a string and match case if requested
|
||||
func find_in_line(line: String, text: String, from_index: int = 0) -> int:
|
||||
if match_case_button.button_pressed:
|
||||
return line.find(text, from_index)
|
||||
else:
|
||||
return line.findn(text, from_index)
|
||||
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_text_edit_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.is_pressed():
|
||||
match event.as_text():
|
||||
"Ctrl+F", "Command+F":
|
||||
open_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
"Ctrl+Shift+R", "Command+Shift+R":
|
||||
replace_check_button.set_pressed(true)
|
||||
open_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_text_edit_text_changed() -> void:
|
||||
results.clear()
|
||||
|
||||
|
||||
func _on_search_and_replace_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_input_text_changed(new_text: String) -> void:
|
||||
search(new_text)
|
||||
|
||||
|
||||
func _on_previous_button_pressed() -> void:
|
||||
self.result_index = wrapi(result_index - 1, 0, results.size())
|
||||
|
||||
|
||||
func _on_next_button_pressed() -> void:
|
||||
self.result_index = wrapi(result_index + 1, 0, results.size())
|
||||
|
||||
|
||||
func _on_search_and_replace_visibility_changed() -> void:
|
||||
if is_instance_valid(input):
|
||||
if visible:
|
||||
input.grab_focus()
|
||||
var selection = code_edit.get_selected_text()
|
||||
if input.text == "" and selection != "":
|
||||
input.text = selection
|
||||
search(selection)
|
||||
else:
|
||||
search()
|
||||
else:
|
||||
input.text = ""
|
||||
|
||||
|
||||
func _on_input_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.is_pressed():
|
||||
match event.as_text():
|
||||
"Enter":
|
||||
search(input.text)
|
||||
"Escape":
|
||||
emit_signal("close_requested")
|
||||
|
||||
|
||||
func _on_replace_button_pressed() -> void:
|
||||
if result_index == -1: return
|
||||
|
||||
# Replace the selection at result index
|
||||
var r: Array = results[result_index]
|
||||
code_edit.begin_complex_operation()
|
||||
var lines: PackedStringArray = code_edit.text.split("\n")
|
||||
var line: String = lines[r[0]]
|
||||
line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2])
|
||||
lines[r[0]] = line
|
||||
code_edit.text = "\n".join(lines)
|
||||
code_edit.end_complex_operation()
|
||||
code_edit.text_changed.emit()
|
||||
|
||||
search(input.text, result_index)
|
||||
|
||||
|
||||
func _on_replace_all_button_pressed() -> void:
|
||||
if match_case_button.button_pressed:
|
||||
code_edit.text = code_edit.text.replace(input.text, replace_input.text)
|
||||
else:
|
||||
code_edit.text = code_edit.text.replacen(input.text, replace_input.text)
|
||||
search()
|
||||
code_edit.text_changed.emit()
|
||||
|
||||
|
||||
func _on_replace_check_button_toggled(button_pressed: bool) -> void:
|
||||
replace_panel.visible = button_pressed
|
||||
if button_pressed:
|
||||
replace_input.grab_focus()
|
||||
|
||||
|
||||
func _on_input_focus_entered() -> void:
|
||||
if results.size() == 0:
|
||||
search()
|
||||
else:
|
||||
self.result_index = result_index
|
||||
|
||||
|
||||
func _on_match_case_check_box_toggled(button_pressed: bool) -> void:
|
||||
search()
|
||||
|
||||
|
||||
#endregion
|
@@ -0,0 +1 @@
|
||||
uid://cijsmjkq21cdq
|
87
addons/dialogue_manager/components/search_and_replace.tscn
Normal file
87
addons/dialogue_manager/components/search_and_replace.tscn
Normal file
@@ -0,0 +1,87 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cijsmjkq21cdq" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"]
|
||||
|
||||
[node name="SearchAndReplace" type="VBoxContainer"]
|
||||
visible = false
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 31.0
|
||||
grow_horizontal = 2
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_8oj1f")
|
||||
|
||||
[node name="Search" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Input" type="LineEdit" parent="Search"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Text to search for"
|
||||
metadata/_edit_use_custom_anchors = true
|
||||
|
||||
[node name="MatchCaseCheckBox" type="CheckBox" parent="Search"]
|
||||
layout_mode = 2
|
||||
text = "Match case"
|
||||
|
||||
[node name="VSeparator" type="VSeparator" parent="Search"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PreviousButton" type="Button" parent="Search"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Previous"
|
||||
flat = true
|
||||
|
||||
[node name="ResultLabel" type="Label" parent="Search"]
|
||||
layout_mode = 2
|
||||
text = "0 of 0"
|
||||
|
||||
[node name="NextButton" type="Button" parent="Search"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Next"
|
||||
flat = true
|
||||
|
||||
[node name="VSeparator2" type="VSeparator" parent="Search"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ReplaceCheckButton" type="CheckButton" parent="Search"]
|
||||
layout_mode = 2
|
||||
text = "Replace"
|
||||
|
||||
[node name="Replace" type="HBoxContainer" parent="."]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ReplaceLabel" type="Label" parent="Replace"]
|
||||
layout_mode = 2
|
||||
text = "Replace with:"
|
||||
|
||||
[node name="Input" type="LineEdit" parent="Replace"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ReplaceButton" type="Button" parent="Replace"]
|
||||
layout_mode = 2
|
||||
disabled = true
|
||||
text = "Replace"
|
||||
flat = true
|
||||
|
||||
[node name="ReplaceAllButton" type="Button" parent="Replace"]
|
||||
layout_mode = 2
|
||||
disabled = true
|
||||
text = "Replace all"
|
||||
flat = true
|
||||
|
||||
[connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"]
|
||||
[connection signal="visibility_changed" from="." to="." method="_on_search_and_replace_visibility_changed"]
|
||||
[connection signal="focus_entered" from="Search/Input" to="." method="_on_input_focus_entered"]
|
||||
[connection signal="gui_input" from="Search/Input" to="." method="_on_input_gui_input"]
|
||||
[connection signal="text_changed" from="Search/Input" to="." method="_on_input_text_changed"]
|
||||
[connection signal="toggled" from="Search/MatchCaseCheckBox" to="." method="_on_match_case_check_box_toggled"]
|
||||
[connection signal="pressed" from="Search/PreviousButton" to="." method="_on_previous_button_pressed"]
|
||||
[connection signal="pressed" from="Search/NextButton" to="." method="_on_next_button_pressed"]
|
||||
[connection signal="toggled" from="Search/ReplaceCheckButton" to="." method="_on_replace_check_button_toggled"]
|
||||
[connection signal="focus_entered" from="Replace/Input" to="." method="_on_input_focus_entered"]
|
||||
[connection signal="gui_input" from="Replace/Input" to="." method="_on_input_gui_input"]
|
||||
[connection signal="pressed" from="Replace/ReplaceButton" to="." method="_on_replace_button_pressed"]
|
||||
[connection signal="pressed" from="Replace/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]
|
69
addons/dialogue_manager/components/title_list.gd
vendored
Normal file
69
addons/dialogue_manager/components/title_list.gd
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
signal title_selected(title: String)
|
||||
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
|
||||
|
||||
@onready var filter_edit: LineEdit = $FilterEdit
|
||||
@onready var list: ItemList = $List
|
||||
|
||||
var titles: PackedStringArray:
|
||||
set(next_titles):
|
||||
titles = next_titles
|
||||
apply_filter()
|
||||
get:
|
||||
return titles
|
||||
|
||||
var filter: String:
|
||||
set(next_filter):
|
||||
filter = next_filter
|
||||
apply_filter()
|
||||
get:
|
||||
return filter
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
apply_theme()
|
||||
|
||||
filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter")
|
||||
|
||||
|
||||
func select_title(title: String) -> void:
|
||||
list.deselect_all()
|
||||
for i in range(0, list.get_item_count()):
|
||||
if list.get_item_text(i) == title.strip_edges():
|
||||
list.select(i)
|
||||
|
||||
|
||||
func apply_filter() -> void:
|
||||
list.clear()
|
||||
for title in titles:
|
||||
if filter == "" or filter.to_lower() in title.to_lower():
|
||||
list.add_item(title.strip_edges())
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
if is_instance_valid(filter_edit):
|
||||
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||
if is_instance_valid(list):
|
||||
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_theme_changed() -> void:
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_filter_edit_text_changed(new_text: String) -> void:
|
||||
self.filter = new_text
|
||||
|
||||
|
||||
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||
if mouse_button_index == MOUSE_BUTTON_LEFT:
|
||||
var title = list.get_item_text(index)
|
||||
title_selected.emit(title)
|
1
addons/dialogue_manager/components/title_list.gd.uid
Normal file
1
addons/dialogue_manager/components/title_list.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d0k2wndjj0ifm
|
27
addons/dialogue_manager/components/title_list.tscn
Normal file
27
addons/dialogue_manager/components/title_list.tscn
Normal file
@@ -0,0 +1,27 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d0k2wndjj0ifm" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"]
|
||||
|
||||
[node name="TitleList" type="VBoxContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_5qqmd")
|
||||
|
||||
[node name="FilterEdit" type="LineEdit" parent="."]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Filter titles"
|
||||
clear_button_enabled = true
|
||||
|
||||
[node name="List" type="ItemList" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
allow_reselect = true
|
||||
|
||||
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
|
||||
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
|
||||
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]
|
125
addons/dialogue_manager/components/update_button.gd
vendored
Normal file
125
addons/dialogue_manager/components/update_button.gd
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
@tool
|
||||
extends Button
|
||||
|
||||
const DialogueConstants = preload("../constants.gd")
|
||||
const DialogueSettings = preload("../settings.gd")
|
||||
|
||||
const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases"
|
||||
|
||||
|
||||
@onready var http_request: HTTPRequest = $HTTPRequest
|
||||
@onready var download_dialog: AcceptDialog = $DownloadDialog
|
||||
@onready var download_update_panel = $DownloadDialog/DownloadUpdatePanel
|
||||
@onready var needs_reload_dialog: AcceptDialog = $NeedsReloadDialog
|
||||
@onready var update_failed_dialog: AcceptDialog = $UpdateFailedDialog
|
||||
@onready var timer: Timer = $Timer
|
||||
|
||||
var needs_reload: bool = false
|
||||
|
||||
# A lambda that gets called just before refreshing the plugin. Return false to stop the reload.
|
||||
var on_before_refresh: Callable = func(): return true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
hide()
|
||||
apply_theme()
|
||||
|
||||
# Check for updates on GitHub
|
||||
check_for_update()
|
||||
|
||||
# Check again every few hours
|
||||
timer.start(60 * 60 * 12)
|
||||
|
||||
|
||||
# Convert a version number to an actually comparable number
|
||||
func version_to_number(version: String) -> int:
|
||||
var bits = version.split(".")
|
||||
return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int()
|
||||
|
||||
|
||||
func apply_theme() -> void:
|
||||
var color: Color = get_theme_color("success_color", "Editor")
|
||||
|
||||
if needs_reload:
|
||||
color = get_theme_color("error_color", "Editor")
|
||||
icon = get_theme_icon("Reload", "EditorIcons")
|
||||
add_theme_color_override("icon_normal_color", color)
|
||||
add_theme_color_override("icon_focus_color", color)
|
||||
add_theme_color_override("icon_hover_color", color)
|
||||
|
||||
add_theme_color_override("font_color", color)
|
||||
add_theme_color_override("font_focus_color", color)
|
||||
add_theme_color_override("font_hover_color", color)
|
||||
|
||||
|
||||
func check_for_update() -> void:
|
||||
if DialogueSettings.get_user_value("check_for_updates", true):
|
||||
http_request.request(REMOTE_RELEASES_URL)
|
||||
|
||||
|
||||
### Signals
|
||||
|
||||
|
||||
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
if result != HTTPRequest.RESULT_SUCCESS: return
|
||||
|
||||
var current_version: String = Engine.get_meta("DialogueManagerPlugin").get_version()
|
||||
|
||||
# Work out the next version from the releases information on GitHub
|
||||
var response = JSON.parse_string(body.get_string_from_utf8())
|
||||
if typeof(response) != TYPE_ARRAY: return
|
||||
|
||||
# GitHub releases are in order of creation, not order of version
|
||||
var versions = (response as Array).filter(func(release):
|
||||
var version: String = release.tag_name.substr(1)
|
||||
var major_version: int = version.split(".")[0].to_int()
|
||||
var current_major_version: int = current_version.split(".")[0].to_int()
|
||||
return major_version == current_major_version and version_to_number(version) > version_to_number(current_version)
|
||||
)
|
||||
if versions.size() > 0:
|
||||
download_update_panel.next_version_release = versions[0]
|
||||
text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) })
|
||||
show()
|
||||
|
||||
|
||||
func _on_update_button_pressed() -> void:
|
||||
if needs_reload:
|
||||
var will_refresh = on_before_refresh.call()
|
||||
if will_refresh:
|
||||
EditorInterface.restart_editor(true)
|
||||
else:
|
||||
var scale: float = EditorInterface.get_editor_scale()
|
||||
download_dialog.min_size = Vector2(300, 250) * scale
|
||||
download_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_download_dialog_close_requested() -> void:
|
||||
download_dialog.hide()
|
||||
|
||||
|
||||
func _on_download_update_panel_updated(updated_to_version: String) -> void:
|
||||
download_dialog.hide()
|
||||
|
||||
needs_reload_dialog.dialog_text = DialogueConstants.translate(&"update.needs_reload")
|
||||
needs_reload_dialog.ok_button_text = DialogueConstants.translate(&"update.reload_ok_button")
|
||||
needs_reload_dialog.cancel_button_text = DialogueConstants.translate(&"update.reload_cancel_button")
|
||||
needs_reload_dialog.popup_centered()
|
||||
|
||||
needs_reload = true
|
||||
text = DialogueConstants.translate(&"update.reload_project")
|
||||
apply_theme()
|
||||
|
||||
|
||||
func _on_download_update_panel_failed() -> void:
|
||||
download_dialog.hide()
|
||||
update_failed_dialog.dialog_text = DialogueConstants.translate(&"update.failed")
|
||||
update_failed_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_needs_reload_dialog_confirmed() -> void:
|
||||
EditorInterface.restart_editor(true)
|
||||
|
||||
|
||||
func _on_timer_timeout() -> void:
|
||||
if not needs_reload:
|
||||
check_for_update()
|
1
addons/dialogue_manager/components/update_button.gd.uid
Normal file
1
addons/dialogue_manager/components/update_button.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cr1tt12dh5ecr
|
42
addons/dialogue_manager/components/update_button.tscn
Normal file
42
addons/dialogue_manager/components/update_button.tscn
Normal file
@@ -0,0 +1,42 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cr1tt12dh5ecr" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"]
|
||||
[ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"]
|
||||
|
||||
[node name="UpdateButton" type="Button"]
|
||||
visible = false
|
||||
offset_right = 8.0
|
||||
offset_bottom = 8.0
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
|
||||
theme_override_colors/font_focus_color = Color(0, 0, 0, 1)
|
||||
text = "v2.9.0 available"
|
||||
flat = true
|
||||
script = ExtResource("1_d2tpb")
|
||||
|
||||
[node name="HTTPRequest" type="HTTPRequest" parent="."]
|
||||
|
||||
[node name="DownloadDialog" type="AcceptDialog" parent="."]
|
||||
title = "Download update"
|
||||
size = Vector2i(400, 300)
|
||||
unresizable = true
|
||||
min_size = Vector2i(300, 250)
|
||||
ok_button_text = "Close"
|
||||
|
||||
[node name="DownloadUpdatePanel" parent="DownloadDialog" instance=ExtResource("2_iwm7r")]
|
||||
|
||||
[node name="UpdateFailedDialog" type="AcceptDialog" parent="."]
|
||||
dialog_text = "You have been updated to version 2.4.3"
|
||||
|
||||
[node name="NeedsReloadDialog" type="ConfirmationDialog" parent="."]
|
||||
|
||||
[node name="Timer" type="Timer" parent="."]
|
||||
wait_time = 14400.0
|
||||
|
||||
[connection signal="pressed" from="." to="." method="_on_update_button_pressed"]
|
||||
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
|
||||
[connection signal="close_requested" from="DownloadDialog" to="." method="_on_download_dialog_close_requested"]
|
||||
[connection signal="failed" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_failed"]
|
||||
[connection signal="updated" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_updated"]
|
||||
[connection signal="confirmed" from="NeedsReloadDialog" to="." method="_on_needs_reload_dialog_confirmed"]
|
||||
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
|
231
addons/dialogue_manager/constants.gd
vendored
Normal file
231
addons/dialogue_manager/constants.gd
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
class_name DMConstants extends RefCounted
|
||||
|
||||
|
||||
const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json"
|
||||
const CACHE_PATH = "user://dialogue_manager_cache.json"
|
||||
|
||||
|
||||
enum MutationBehaviour {
|
||||
Wait,
|
||||
DoNotWait,
|
||||
Skip
|
||||
}
|
||||
|
||||
enum TranslationSource {
|
||||
None,
|
||||
Guess,
|
||||
CSV,
|
||||
PO
|
||||
}
|
||||
|
||||
# Token types
|
||||
|
||||
const TOKEN_FUNCTION = &"function"
|
||||
const TOKEN_DICTIONARY_REFERENCE = &"dictionary_reference"
|
||||
const TOKEN_DICTIONARY_NESTED_REFERENCE = &"dictionary_nested_reference"
|
||||
const TOKEN_GROUP = &"group"
|
||||
const TOKEN_ARRAY = &"array"
|
||||
const TOKEN_DICTIONARY = &"dictionary"
|
||||
const TOKEN_PARENS_OPEN = &"parens_open"
|
||||
const TOKEN_PARENS_CLOSE = &"parens_close"
|
||||
const TOKEN_BRACKET_OPEN = &"bracket_open"
|
||||
const TOKEN_BRACKET_CLOSE = &"bracket_close"
|
||||
const TOKEN_BRACE_OPEN = &"brace_open"
|
||||
const TOKEN_BRACE_CLOSE = &"brace_close"
|
||||
const TOKEN_COLON = &"colon"
|
||||
const TOKEN_COMPARISON = &"comparison"
|
||||
const TOKEN_ASSIGNMENT = &"assignment"
|
||||
const TOKEN_OPERATOR = &"operator"
|
||||
const TOKEN_COMMA = &"comma"
|
||||
const TOKEN_NULL_COALESCE = &"null_coalesce"
|
||||
const TOKEN_DOT = &"dot"
|
||||
const TOKEN_CONDITION = &"condition"
|
||||
const TOKEN_BOOL = &"bool"
|
||||
const TOKEN_NOT = &"not"
|
||||
const TOKEN_AND_OR = &"and_or"
|
||||
const TOKEN_STRING = &"string"
|
||||
const TOKEN_NUMBER = &"number"
|
||||
const TOKEN_VARIABLE = &"variable"
|
||||
const TOKEN_COMMENT = &"comment"
|
||||
|
||||
const TOKEN_VALUE = &"value"
|
||||
const TOKEN_ERROR = &"error"
|
||||
|
||||
# Line types
|
||||
|
||||
const TYPE_UNKNOWN = &""
|
||||
const TYPE_IMPORT = &"import"
|
||||
const TYPE_USING = &"using"
|
||||
const TYPE_COMMENT = &"comment"
|
||||
const TYPE_RESPONSE = &"response"
|
||||
const TYPE_TITLE = &"title"
|
||||
const TYPE_CONDITION = &"condition"
|
||||
const TYPE_WHILE = &"while"
|
||||
const TYPE_MATCH = &"match"
|
||||
const TYPE_WHEN = &"when"
|
||||
const TYPE_MUTATION = &"mutation"
|
||||
const TYPE_GOTO = &"goto"
|
||||
const TYPE_DIALOGUE = &"dialogue"
|
||||
const TYPE_RANDOM = &"random"
|
||||
const TYPE_ERROR = &"error"
|
||||
|
||||
# Line IDs
|
||||
|
||||
const ID_NULL = &""
|
||||
const ID_ERROR = &"error"
|
||||
const ID_ERROR_INVALID_TITLE = &"invalid title"
|
||||
const ID_ERROR_TITLE_HAS_NO_BODY = &"title has no body"
|
||||
const ID_END = &"end"
|
||||
const ID_END_CONVERSATION = &"end!"
|
||||
|
||||
# Errors
|
||||
|
||||
const ERR_ERRORS_IN_IMPORTED_FILE = 100
|
||||
const ERR_FILE_ALREADY_IMPORTED = 101
|
||||
const ERR_DUPLICATE_IMPORT_NAME = 102
|
||||
const ERR_EMPTY_TITLE = 103
|
||||
const ERR_DUPLICATE_TITLE = 104
|
||||
const ERR_TITLE_INVALID_CHARACTERS = 106
|
||||
const ERR_UNKNOWN_TITLE = 107
|
||||
const ERR_INVALID_TITLE_REFERENCE = 108
|
||||
const ERR_TITLE_REFERENCE_HAS_NO_CONTENT = 109
|
||||
const ERR_INVALID_EXPRESSION = 110
|
||||
const ERR_UNEXPECTED_CONDITION = 111
|
||||
const ERR_DUPLICATE_ID = 112
|
||||
const ERR_MISSING_ID = 113
|
||||
const ERR_INVALID_INDENTATION = 114
|
||||
const ERR_INVALID_CONDITION_INDENTATION = 115
|
||||
const ERR_INCOMPLETE_EXPRESSION = 116
|
||||
const ERR_INVALID_EXPRESSION_FOR_VALUE = 117
|
||||
const ERR_UNKNOWN_LINE_SYNTAX = 118
|
||||
const ERR_TITLE_BEGINS_WITH_NUMBER = 119
|
||||
const ERR_UNEXPECTED_END_OF_EXPRESSION = 120
|
||||
const ERR_UNEXPECTED_FUNCTION = 121
|
||||
const ERR_UNEXPECTED_BRACKET = 122
|
||||
const ERR_UNEXPECTED_CLOSING_BRACKET = 123
|
||||
const ERR_MISSING_CLOSING_BRACKET = 124
|
||||
const ERR_UNEXPECTED_OPERATOR = 125
|
||||
const ERR_UNEXPECTED_COMMA = 126
|
||||
const ERR_UNEXPECTED_COLON = 127
|
||||
const ERR_UNEXPECTED_DOT = 128
|
||||
const ERR_UNEXPECTED_BOOLEAN = 129
|
||||
const ERR_UNEXPECTED_STRING = 130
|
||||
const ERR_UNEXPECTED_NUMBER = 131
|
||||
const ERR_UNEXPECTED_VARIABLE = 132
|
||||
const ERR_INVALID_INDEX = 133
|
||||
const ERR_UNEXPECTED_ASSIGNMENT = 134
|
||||
const ERR_UNKNOWN_USING = 135
|
||||
const ERR_EXPECTED_WHEN_OR_ELSE = 136
|
||||
const ERR_ONLY_ONE_ELSE_ALLOWED = 137
|
||||
const ERR_WHEN_MUST_BELONG_TO_MATCH = 138
|
||||
const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139
|
||||
const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140
|
||||
const ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE = 141
|
||||
const ERR_NESTED_DIALOGUE_INVALID_JUMP = 142
|
||||
|
||||
|
||||
static var _current_locale: String = ""
|
||||
static var _current_translation: Translation
|
||||
|
||||
|
||||
## Get the error message
|
||||
static func get_error_message(error: int) -> String:
|
||||
match error:
|
||||
ERR_ERRORS_IN_IMPORTED_FILE:
|
||||
return translate(&"errors.import_errors")
|
||||
ERR_FILE_ALREADY_IMPORTED:
|
||||
return translate(&"errors.already_imported")
|
||||
ERR_DUPLICATE_IMPORT_NAME:
|
||||
return translate(&"errors.duplicate_import")
|
||||
ERR_EMPTY_TITLE:
|
||||
return translate(&"errors.empty_title")
|
||||
ERR_DUPLICATE_TITLE:
|
||||
return translate(&"errors.duplicate_title")
|
||||
ERR_TITLE_INVALID_CHARACTERS:
|
||||
return translate(&"errors.invalid_title_string")
|
||||
ERR_TITLE_BEGINS_WITH_NUMBER:
|
||||
return translate(&"errors.invalid_title_number")
|
||||
ERR_UNKNOWN_TITLE:
|
||||
return translate(&"errors.unknown_title")
|
||||
ERR_INVALID_TITLE_REFERENCE:
|
||||
return translate(&"errors.jump_to_invalid_title")
|
||||
ERR_TITLE_REFERENCE_HAS_NO_CONTENT:
|
||||
return translate(&"errors.title_has_no_content")
|
||||
ERR_INVALID_EXPRESSION:
|
||||
return translate(&"errors.invalid_expression")
|
||||
ERR_UNEXPECTED_CONDITION:
|
||||
return translate(&"errors.unexpected_condition")
|
||||
ERR_DUPLICATE_ID:
|
||||
return translate(&"errors.duplicate_id")
|
||||
ERR_MISSING_ID:
|
||||
return translate(&"errors.missing_id")
|
||||
ERR_INVALID_INDENTATION:
|
||||
return translate(&"errors.invalid_indentation")
|
||||
ERR_INVALID_CONDITION_INDENTATION:
|
||||
return translate(&"errors.condition_has_no_content")
|
||||
ERR_INCOMPLETE_EXPRESSION:
|
||||
return translate(&"errors.incomplete_expression")
|
||||
ERR_INVALID_EXPRESSION_FOR_VALUE:
|
||||
return translate(&"errors.invalid_expression_for_value")
|
||||
ERR_FILE_NOT_FOUND:
|
||||
return translate(&"errors.file_not_found")
|
||||
ERR_UNEXPECTED_END_OF_EXPRESSION:
|
||||
return translate(&"errors.unexpected_end_of_expression")
|
||||
ERR_UNEXPECTED_FUNCTION:
|
||||
return translate(&"errors.unexpected_function")
|
||||
ERR_UNEXPECTED_BRACKET:
|
||||
return translate(&"errors.unexpected_bracket")
|
||||
ERR_UNEXPECTED_CLOSING_BRACKET:
|
||||
return translate(&"errors.unexpected_closing_bracket")
|
||||
ERR_MISSING_CLOSING_BRACKET:
|
||||
return translate(&"errors.missing_closing_bracket")
|
||||
ERR_UNEXPECTED_OPERATOR:
|
||||
return translate(&"errors.unexpected_operator")
|
||||
ERR_UNEXPECTED_COMMA:
|
||||
return translate(&"errors.unexpected_comma")
|
||||
ERR_UNEXPECTED_COLON:
|
||||
return translate(&"errors.unexpected_colon")
|
||||
ERR_UNEXPECTED_DOT:
|
||||
return translate(&"errors.unexpected_dot")
|
||||
ERR_UNEXPECTED_BOOLEAN:
|
||||
return translate(&"errors.unexpected_boolean")
|
||||
ERR_UNEXPECTED_STRING:
|
||||
return translate(&"errors.unexpected_string")
|
||||
ERR_UNEXPECTED_NUMBER:
|
||||
return translate(&"errors.unexpected_number")
|
||||
ERR_UNEXPECTED_VARIABLE:
|
||||
return translate(&"errors.unexpected_variable")
|
||||
ERR_INVALID_INDEX:
|
||||
return translate(&"errors.invalid_index")
|
||||
ERR_UNEXPECTED_ASSIGNMENT:
|
||||
return translate(&"errors.unexpected_assignment")
|
||||
ERR_UNKNOWN_USING:
|
||||
return translate(&"errors.unknown_using")
|
||||
ERR_EXPECTED_WHEN_OR_ELSE:
|
||||
return translate(&"errors.expected_when_or_else")
|
||||
ERR_ONLY_ONE_ELSE_ALLOWED:
|
||||
return translate(&"errors.only_one_else_allowed")
|
||||
ERR_WHEN_MUST_BELONG_TO_MATCH:
|
||||
return translate(&"errors.when_must_belong_to_match")
|
||||
ERR_CONCURRENT_LINE_WITHOUT_ORIGIN:
|
||||
return translate(&"errors.concurrent_line_without_origin")
|
||||
ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES:
|
||||
return translate(&"errors.goto_not_allowed_on_concurrect_lines")
|
||||
ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE:
|
||||
return translate(&"errors.unexpected_syntax_on_nested_dialogue_line")
|
||||
ERR_NESTED_DIALOGUE_INVALID_JUMP:
|
||||
return translate(&"errors.err_nested_dialogue_invalid_jump")
|
||||
|
||||
return translate(&"errors.unknown")
|
||||
|
||||
|
||||
static func translate(string: String) -> String:
|
||||
var locale: String = TranslationServer.get_tool_locale()
|
||||
if _current_translation == null or _current_locale != locale:
|
||||
var base_path: String = new().get_script().resource_path.get_base_dir()
|
||||
var translation_path: String = "%s/l10n/%s.po" % [base_path, locale]
|
||||
var fallback_translation_path: String = "%s/l10n/%s.po" % [base_path, locale.substr(0, 2)]
|
||||
var en_translation_path: String = "%s/l10n/en.po" % base_path
|
||||
_current_translation = load(translation_path if FileAccess.file_exists(translation_path) else (fallback_translation_path if FileAccess.file_exists(fallback_translation_path) else en_translation_path))
|
||||
_current_locale = locale
|
||||
return _current_translation.get_message(string)
|
1
addons/dialogue_manager/constants.gd.uid
Normal file
1
addons/dialogue_manager/constants.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b1oarbmjtyesf
|
232
addons/dialogue_manager/dialogue_label.gd
vendored
Normal file
232
addons/dialogue_manager/dialogue_label.gd
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
@icon("./assets/icon.svg")
|
||||
|
||||
@tool
|
||||
|
||||
## A RichTextLabel specifically for use with [b]Dialogue Manager[/b] dialogue.
|
||||
class_name DialogueLabel extends RichTextLabel
|
||||
|
||||
|
||||
## Emitted for each letter typed out.
|
||||
signal spoke(letter: String, letter_index: int, speed: float)
|
||||
|
||||
## Emitted when typing paused for a `[wait]`
|
||||
signal paused_typing(duration: float)
|
||||
|
||||
## Emitted when the player skips the typing of dialogue.
|
||||
signal skipped_typing()
|
||||
|
||||
## Emitted when typing finishes.
|
||||
signal finished_typing()
|
||||
|
||||
|
||||
# The action to press to skip typing.
|
||||
@export var skip_action: StringName = &"ui_cancel"
|
||||
|
||||
## The speed with which the text types out.
|
||||
@export var seconds_per_step: float = 0.02
|
||||
|
||||
## Automatically have a brief pause when these characters are encountered.
|
||||
@export var pause_at_characters: String = ".?!"
|
||||
|
||||
## Don't auto pause if the character after the pause is one of these.
|
||||
@export var skip_pause_at_character_if_followed_by: String = ")\""
|
||||
|
||||
## Don't auto pause after these abbreviations (only if "." is in `pause_at_characters`).[br]
|
||||
## Abbreviations are limitted to 5 characters in length [br]
|
||||
## Does not support multi-period abbreviations (ex. "p.m.")
|
||||
@export var skip_pause_at_abbreviations: PackedStringArray = ["Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex"]
|
||||
|
||||
## The amount of time to pause when exposing a character present in `pause_at_characters`.
|
||||
@export var seconds_per_pause_step: float = 0.3
|
||||
|
||||
var _already_mutated_indices: PackedInt32Array = []
|
||||
|
||||
|
||||
## The current line of dialogue.
|
||||
var dialogue_line:
|
||||
set(next_dialogue_line):
|
||||
dialogue_line = next_dialogue_line
|
||||
custom_minimum_size = Vector2.ZERO
|
||||
text = ""
|
||||
text = dialogue_line.text
|
||||
get:
|
||||
return dialogue_line
|
||||
|
||||
## Whether the label is currently typing itself out.
|
||||
var is_typing: bool = false:
|
||||
set(value):
|
||||
var is_finished: bool = is_typing != value and value == false
|
||||
is_typing = value
|
||||
if is_finished:
|
||||
finished_typing.emit()
|
||||
get:
|
||||
return is_typing
|
||||
|
||||
var _last_wait_index: int = -1
|
||||
var _last_mutation_index: int = -1
|
||||
var _waiting_seconds: float = 0
|
||||
var _is_awaiting_mutation: bool = false
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if self.is_typing:
|
||||
# Type out text
|
||||
if visible_ratio < 1:
|
||||
# See if we are waiting
|
||||
if _waiting_seconds > 0:
|
||||
_waiting_seconds = _waiting_seconds - delta
|
||||
# If we are no longer waiting then keep typing
|
||||
if _waiting_seconds <= 0:
|
||||
_type_next(delta, _waiting_seconds)
|
||||
else:
|
||||
# Make sure any mutations at the end of the line get run
|
||||
_mutate_inline_mutations(get_total_character_count())
|
||||
self.is_typing = false
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# Note: this will no longer be reached if using Dialogue Manager > 2.32.2. To make skip handling
|
||||
# simpler (so all of mouse/keyboard/joypad are together) it is now the responsibility of the
|
||||
# dialogue balloon.
|
||||
if self.is_typing and visible_ratio < 1 and InputMap.has_action(skip_action) and event.is_action_pressed(skip_action):
|
||||
get_viewport().set_input_as_handled()
|
||||
skip_typing()
|
||||
|
||||
|
||||
## Start typing out the text
|
||||
func type_out() -> void:
|
||||
text = dialogue_line.text
|
||||
visible_characters = 0
|
||||
visible_ratio = 0
|
||||
_waiting_seconds = 0
|
||||
_last_wait_index = -1
|
||||
_last_mutation_index = -1
|
||||
_already_mutated_indices.clear()
|
||||
|
||||
self.is_typing = true
|
||||
|
||||
# Allow typing listeners a chance to connect
|
||||
await get_tree().process_frame
|
||||
|
||||
if get_total_character_count() == 0:
|
||||
self.is_typing = false
|
||||
elif seconds_per_step == 0:
|
||||
_mutate_remaining_mutations()
|
||||
visible_characters = get_total_character_count()
|
||||
self.is_typing = false
|
||||
|
||||
|
||||
## Stop typing out the text and jump right to the end
|
||||
func skip_typing() -> void:
|
||||
_mutate_remaining_mutations()
|
||||
visible_characters = get_total_character_count()
|
||||
self.is_typing = false
|
||||
skipped_typing.emit()
|
||||
|
||||
|
||||
# Type out the next character(s)
|
||||
func _type_next(delta: float, seconds_needed: float) -> void:
|
||||
if _is_awaiting_mutation: return
|
||||
|
||||
if visible_characters == get_total_character_count():
|
||||
return
|
||||
|
||||
if _last_mutation_index != visible_characters:
|
||||
_last_mutation_index = visible_characters
|
||||
_mutate_inline_mutations(visible_characters)
|
||||
if _is_awaiting_mutation: return
|
||||
|
||||
var additional_waiting_seconds: float = _get_pause(visible_characters)
|
||||
|
||||
# Pause on characters like "."
|
||||
if _should_auto_pause():
|
||||
additional_waiting_seconds += seconds_per_pause_step
|
||||
|
||||
# Pause at literal [wait] directives
|
||||
if _last_wait_index != visible_characters and additional_waiting_seconds > 0:
|
||||
_last_wait_index = visible_characters
|
||||
_waiting_seconds += additional_waiting_seconds
|
||||
paused_typing.emit(_get_pause(visible_characters))
|
||||
else:
|
||||
visible_characters += 1
|
||||
if visible_characters <= get_total_character_count():
|
||||
spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, _get_speed(visible_characters))
|
||||
# See if there's time to type out some more in this frame
|
||||
seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters))
|
||||
if seconds_needed > delta:
|
||||
_waiting_seconds += seconds_needed
|
||||
else:
|
||||
_type_next(delta, seconds_needed)
|
||||
|
||||
|
||||
# Get the pause for the current typing position if there is one
|
||||
func _get_pause(at_index: int) -> float:
|
||||
return dialogue_line.pauses.get(at_index, 0)
|
||||
|
||||
|
||||
# Get the speed for the current typing position
|
||||
func _get_speed(at_index: int) -> float:
|
||||
var speed: float = 1
|
||||
for index in dialogue_line.speeds:
|
||||
if index > at_index:
|
||||
return speed
|
||||
speed = dialogue_line.speeds[index]
|
||||
return speed
|
||||
|
||||
|
||||
# Run any inline mutations that haven't been run yet
|
||||
func _mutate_remaining_mutations() -> void:
|
||||
for i in range(visible_characters, get_total_character_count() + 1):
|
||||
_mutate_inline_mutations(i)
|
||||
|
||||
|
||||
# Run any mutations at the current typing position
|
||||
func _mutate_inline_mutations(index: int) -> void:
|
||||
for inline_mutation in dialogue_line.inline_mutations:
|
||||
# inline mutations are an array of arrays in the form of [character index, resolvable function]
|
||||
if inline_mutation[0] > index:
|
||||
return
|
||||
if inline_mutation[0] == index and not _already_mutated_indices.has(index):
|
||||
_is_awaiting_mutation = true
|
||||
# The DialogueManager can't be referenced directly here so we need to get it by its path
|
||||
await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
|
||||
_is_awaiting_mutation = false
|
||||
|
||||
_already_mutated_indices.append(index)
|
||||
|
||||
|
||||
# Determine if the current autopause character at the cursor should qualify to pause typing.
|
||||
func _should_auto_pause() -> bool:
|
||||
if visible_characters == 0: return false
|
||||
|
||||
var parsed_text: String = get_parsed_text()
|
||||
|
||||
# Avoid outofbounds when the label auto-translates and the text changes to one shorter while typing out
|
||||
# Note: visible characters can be larger than parsed_text after a translation event
|
||||
if visible_characters >= parsed_text.length(): return false
|
||||
|
||||
# Ignore pause characters if they are next to a non-pause character
|
||||
if parsed_text[visible_characters] in skip_pause_at_character_if_followed_by.split():
|
||||
return false
|
||||
|
||||
# Ignore "." if it's between two numbers
|
||||
if visible_characters > 3 and parsed_text[visible_characters - 1] == ".":
|
||||
var possible_number: String = parsed_text.substr(visible_characters - 2, 3)
|
||||
if str(float(possible_number)).pad_decimals(1) == possible_number:
|
||||
return false
|
||||
|
||||
# Ignore "." if it's used in an abbreviation
|
||||
# Note: does NOT support multi-period abbreviations (ex. p.m.)
|
||||
if "." in pause_at_characters and parsed_text[visible_characters - 1] == ".":
|
||||
for abbreviation in skip_pause_at_abbreviations:
|
||||
if visible_characters >= abbreviation.length():
|
||||
var previous_characters: String = parsed_text.substr(visible_characters - abbreviation.length() - 1, abbreviation.length())
|
||||
if previous_characters == abbreviation:
|
||||
return false
|
||||
|
||||
# Ignore two non-"." characters next to each other
|
||||
var other_pause_characters: PackedStringArray = pause_at_characters.replace(".", "").split()
|
||||
if visible_characters > 1 and parsed_text[visible_characters - 1] in other_pause_characters and parsed_text[visible_characters] in other_pause_characters:
|
||||
return false
|
||||
|
||||
return parsed_text[visible_characters - 1] in pause_at_characters.split()
|
1
addons/dialogue_manager/dialogue_label.gd.uid
Normal file
1
addons/dialogue_manager/dialogue_label.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://g32um0mltv5d
|
19
addons/dialogue_manager/dialogue_label.tscn
Normal file
19
addons/dialogue_manager/dialogue_label.tscn
Normal file
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://g32um0mltv5d" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"]
|
||||
|
||||
[node name="DialogueLabel" type="RichTextLabel"]
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
shortcut_keys_enabled = false
|
||||
meta_underlined = false
|
||||
hint_underlined = false
|
||||
deselect_on_focus_loss_enabled = false
|
||||
visible_characters_behavior = 1
|
||||
script = ExtResource("1_cital")
|
||||
skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex")
|
99
addons/dialogue_manager/dialogue_line.gd
vendored
Normal file
99
addons/dialogue_manager/dialogue_line.gd
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
## A line of dialogue returned from [code]DialogueManager[/code].
|
||||
class_name DialogueLine extends RefCounted
|
||||
|
||||
|
||||
## The ID of this line
|
||||
var id: String
|
||||
|
||||
## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code]
|
||||
var type: String = DMConstants.TYPE_DIALOGUE
|
||||
|
||||
## The next line ID after this line.
|
||||
var next_id: String = ""
|
||||
|
||||
## The character name that is saying this line.
|
||||
var character: String = ""
|
||||
|
||||
## A dictionary of variable replacements fo the character name. Generally for internal use only.
|
||||
var character_replacements: Array[Dictionary] = []
|
||||
|
||||
## The dialogue being spoken.
|
||||
var text: String = ""
|
||||
|
||||
## A dictionary of replacements for the text. Generally for internal use only.
|
||||
var text_replacements: Array[Dictionary] = []
|
||||
|
||||
## The key to use for translating this line.
|
||||
var translation_key: String = ""
|
||||
|
||||
## A map for when and for how long to pause while typing out the dialogue text.
|
||||
var pauses: Dictionary = {}
|
||||
|
||||
## A map for speed changes when typing out the dialogue text.
|
||||
var speeds: Dictionary = {}
|
||||
|
||||
## A map of any mutations to run while typing out the dialogue text.
|
||||
var inline_mutations: Array[Array] = []
|
||||
|
||||
## A list of responses attached to this line of dialogue.
|
||||
var responses: Array = []
|
||||
|
||||
## A list of lines that are spoken simultaneously with this one.
|
||||
var concurrent_lines: Array[DialogueLine] = []
|
||||
|
||||
## A list of any extra game states to check when resolving variables and mutations.
|
||||
var extra_game_states: Array = []
|
||||
|
||||
## How long to show this line before advancing to the next. Either a float (of seconds), [code]"auto"[/code], or [code]null[/code].
|
||||
var time: String = ""
|
||||
|
||||
## Any #tags that were included in the line
|
||||
var tags: PackedStringArray = []
|
||||
|
||||
## The mutation details if this is a mutation line (where [code]type == TYPE_MUTATION[/code]).
|
||||
var mutation: Dictionary = {}
|
||||
|
||||
## The conditions to check before including this line in the flow of dialogue. If failed the line will be skipped over.
|
||||
var conditions: Dictionary = {}
|
||||
|
||||
|
||||
func _init(data: Dictionary = {}) -> void:
|
||||
if data.size() > 0:
|
||||
id = data.id
|
||||
next_id = data.next_id
|
||||
type = data.type
|
||||
extra_game_states = data.get("extra_game_states", [])
|
||||
|
||||
match type:
|
||||
DMConstants.TYPE_DIALOGUE:
|
||||
character = data.character
|
||||
character_replacements = data.get("character_replacements", [] as Array[Dictionary])
|
||||
text = data.text
|
||||
text_replacements = data.get("text_replacements", [] as Array[Dictionary])
|
||||
translation_key = data.get("translation_key", data.text)
|
||||
pauses = data.get("pauses", {})
|
||||
speeds = data.get("speeds", {})
|
||||
inline_mutations = data.get("inline_mutations", [] as Array[Array])
|
||||
time = data.get("time", "")
|
||||
tags = data.get("tags", [])
|
||||
concurrent_lines = data.get("concurrent_lines", [] as Array[DialogueLine])
|
||||
|
||||
DMConstants.TYPE_MUTATION:
|
||||
mutation = data.mutation
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
match type:
|
||||
DMConstants.TYPE_DIALOGUE:
|
||||
return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text]
|
||||
DMConstants.TYPE_MUTATION:
|
||||
return "<DialogueLine mutation>"
|
||||
return ""
|
||||
|
||||
|
||||
func get_tag_value(tag_name: String) -> String:
|
||||
var wrapped := "%s=" % tag_name
|
||||
for t in tags:
|
||||
if t.begins_with(wrapped):
|
||||
return t.replace(wrapped, "").strip_edges()
|
||||
return ""
|
1
addons/dialogue_manager/dialogue_line.gd.uid
Normal file
1
addons/dialogue_manager/dialogue_line.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://rhuq0eyf8ar2
|
1483
addons/dialogue_manager/dialogue_manager.gd
vendored
Normal file
1483
addons/dialogue_manager/dialogue_manager.gd
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
addons/dialogue_manager/dialogue_manager.gd.uid
Normal file
1
addons/dialogue_manager/dialogue_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3rodes2l3gxb
|
42
addons/dialogue_manager/dialogue_resource.gd
vendored
Normal file
42
addons/dialogue_manager/dialogue_resource.gd
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
@tool
|
||||
@icon("./assets/icon.svg")
|
||||
|
||||
## A collection of dialogue lines for use with [code]DialogueManager[/code].
|
||||
class_name DialogueResource extends Resource
|
||||
|
||||
|
||||
const DialogueLine = preload("./dialogue_line.gd")
|
||||
|
||||
## A list of state shortcuts
|
||||
@export var using_states: PackedStringArray = []
|
||||
|
||||
## A map of titles and the lines they point to.
|
||||
@export var titles: Dictionary = {}
|
||||
|
||||
## A list of character names.
|
||||
@export var character_names: PackedStringArray = []
|
||||
|
||||
## The first title in the file.
|
||||
@export var first_title: String = ""
|
||||
|
||||
## A map of the encoded lines of dialogue.
|
||||
@export var lines: Dictionary = {}
|
||||
|
||||
## raw version of the text
|
||||
@export var raw_text: String
|
||||
|
||||
|
||||
## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can
|
||||
## be a title string or a stringified line number). Runs any mutations along the way and then returns
|
||||
## the first dialogue line encountered.
|
||||
func get_next_dialogue_line(title: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine:
|
||||
return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour)
|
||||
|
||||
|
||||
## Get the list of any titles found in the file.
|
||||
func get_titles() -> PackedStringArray:
|
||||
return titles.keys()
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "<DialogueResource titles=\"%s\">" % [",".join(titles.keys())]
|
1
addons/dialogue_manager/dialogue_resource.gd.uid
Normal file
1
addons/dialogue_manager/dialogue_resource.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dbs4435dsf3ry
|
63
addons/dialogue_manager/dialogue_response.gd
vendored
Normal file
63
addons/dialogue_manager/dialogue_response.gd
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
## A response to a line of dialogue, usualy attached to a [code]DialogueLine[/code].
|
||||
class_name DialogueResponse extends RefCounted
|
||||
|
||||
|
||||
## The ID of this response
|
||||
var id: String
|
||||
|
||||
## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code].
|
||||
var type: String = DMConstants.TYPE_RESPONSE
|
||||
|
||||
## The next line ID to use if this response is selected by the player.
|
||||
var next_id: String = ""
|
||||
|
||||
## [code]true[/code] if the condition of this line was met.
|
||||
var is_allowed: bool = true
|
||||
|
||||
## The original condition text.
|
||||
var condition_as_text: String = ""
|
||||
|
||||
## A character (depending on the "characters in responses" behaviour setting).
|
||||
var character: String = ""
|
||||
|
||||
## A dictionary of varialbe replaces for the character name. Generally for internal use only.
|
||||
var character_replacements: Array[Dictionary] = []
|
||||
|
||||
## The prompt for this response.
|
||||
var text: String = ""
|
||||
|
||||
## A dictionary of variable replaces for the text. Generally for internal use only.
|
||||
var text_replacements: Array[Dictionary] = []
|
||||
|
||||
## Any #tags
|
||||
var tags: PackedStringArray = []
|
||||
|
||||
## The key to use for translating the text.
|
||||
var translation_key: String = ""
|
||||
|
||||
|
||||
func _init(data: Dictionary = {}) -> void:
|
||||
if data.size() > 0:
|
||||
id = data.id
|
||||
type = data.type
|
||||
next_id = data.next_id
|
||||
is_allowed = data.is_allowed
|
||||
character = data.character
|
||||
character_replacements = data.character_replacements
|
||||
text = data.text
|
||||
text_replacements = data.text_replacements
|
||||
tags = data.tags
|
||||
translation_key = data.translation_key
|
||||
condition_as_text = data.condition_as_text
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "<DialogueResponse text=\"%s\">" % text
|
||||
|
||||
|
||||
func get_tag_value(tag_name: String) -> String:
|
||||
var wrapped := "%s=" % tag_name
|
||||
for t in tags:
|
||||
if t.begins_with(wrapped):
|
||||
return t.replace(wrapped, "").strip_edges()
|
||||
return ""
|
1
addons/dialogue_manager/dialogue_response.gd.uid
Normal file
1
addons/dialogue_manager/dialogue_response.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cm0xpfeywpqid
|
147
addons/dialogue_manager/dialogue_responses_menu.gd
vendored
Normal file
147
addons/dialogue_manager/dialogue_responses_menu.gd
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
@icon("./assets/responses_menu.svg")
|
||||
|
||||
## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b].
|
||||
class_name DialogueResponsesMenu extends Container
|
||||
|
||||
|
||||
## Emitted when a response is selected.
|
||||
signal response_selected(response)
|
||||
|
||||
|
||||
## Optionally specify a control to duplicate for each response
|
||||
@export var response_template: Control
|
||||
|
||||
## The action for accepting a response (is possibly overridden by parent dialogue balloon).
|
||||
@export var next_action: StringName = &""
|
||||
|
||||
## Hide any responses where [code]is_allowed[/code] is false
|
||||
@export var hide_failed_responses: bool = false
|
||||
|
||||
## The list of dialogue responses.
|
||||
var responses: Array = []:
|
||||
get:
|
||||
return responses
|
||||
set(value):
|
||||
responses = value
|
||||
|
||||
# Remove any current items
|
||||
for item in get_children():
|
||||
if item == response_template: continue
|
||||
|
||||
remove_child(item)
|
||||
item.queue_free()
|
||||
|
||||
# Add new items
|
||||
if responses.size() > 0:
|
||||
for response in responses:
|
||||
if hide_failed_responses and not response.is_allowed: continue
|
||||
|
||||
var item: Control
|
||||
if is_instance_valid(response_template):
|
||||
item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS)
|
||||
item.show()
|
||||
else:
|
||||
item = Button.new()
|
||||
item.name = "Response%d" % get_child_count()
|
||||
if not response.is_allowed:
|
||||
item.name = item.name + &"Disallowed"
|
||||
item.disabled = true
|
||||
|
||||
# If the item has a response property then use that
|
||||
if "response" in item:
|
||||
item.response = response
|
||||
# Otherwise assume we can just set the text
|
||||
else:
|
||||
item.text = response.text
|
||||
|
||||
item.set_meta("response", response)
|
||||
|
||||
add_child(item)
|
||||
|
||||
_configure_focus()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visibility_changed.connect(func():
|
||||
if visible and get_menu_items().size() > 0:
|
||||
var first_item: Control = get_menu_items()[0]
|
||||
if first_item.is_inside_tree():
|
||||
first_item.grab_focus()
|
||||
)
|
||||
|
||||
if is_instance_valid(response_template):
|
||||
response_template.hide()
|
||||
|
||||
|
||||
## Get the selectable items in the menu.
|
||||
func get_menu_items() -> Array:
|
||||
var items: Array = []
|
||||
for child in get_children():
|
||||
if not child.visible: continue
|
||||
if "Disallowed" in child.name: continue
|
||||
items.append(child)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
#region Internal
|
||||
|
||||
|
||||
# Prepare the menu for keyboard and mouse navigation.
|
||||
func _configure_focus() -> void:
|
||||
var items = get_menu_items()
|
||||
for i in items.size():
|
||||
var item: Control = items[i]
|
||||
|
||||
item.focus_mode = Control.FOCUS_ALL
|
||||
|
||||
item.focus_neighbor_left = item.get_path()
|
||||
item.focus_neighbor_right = item.get_path()
|
||||
|
||||
if i == 0:
|
||||
item.focus_neighbor_top = item.get_path()
|
||||
item.focus_neighbor_left = item.get_path()
|
||||
item.focus_previous = item.get_path()
|
||||
else:
|
||||
item.focus_neighbor_top = items[i - 1].get_path()
|
||||
item.focus_neighbor_left = items[i - 1].get_path()
|
||||
item.focus_previous = items[i - 1].get_path()
|
||||
|
||||
if i == items.size() - 1:
|
||||
item.focus_neighbor_bottom = item.get_path()
|
||||
item.focus_neighbor_right = item.get_path()
|
||||
item.focus_next = item.get_path()
|
||||
else:
|
||||
item.focus_neighbor_bottom = items[i + 1].get_path()
|
||||
item.focus_neighbor_right = items[i + 1].get_path()
|
||||
item.focus_next = items[i + 1].get_path()
|
||||
|
||||
item.mouse_entered.connect(_on_response_mouse_entered.bind(item))
|
||||
item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response")))
|
||||
|
||||
items[0].grab_focus()
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_response_mouse_entered(item: Control) -> void:
|
||||
if "Disallowed" in item.name: return
|
||||
|
||||
item.grab_focus()
|
||||
|
||||
|
||||
func _on_response_gui_input(event: InputEvent, item: Control, response) -> void:
|
||||
if "Disallowed" in item.name: return
|
||||
|
||||
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
get_viewport().set_input_as_handled()
|
||||
response_selected.emit(response)
|
||||
elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items():
|
||||
get_viewport().set_input_as_handled()
|
||||
response_selected.emit(response)
|
||||
|
||||
|
||||
#endregion
|
1
addons/dialogue_manager/dialogue_responses_menu.gd.uid
Normal file
1
addons/dialogue_manager/dialogue_responses_menu.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bb52rsfwhkxbn
|
53
addons/dialogue_manager/editor_translation_parser_plugin.gd
vendored
Normal file
53
addons/dialogue_manager/editor_translation_parser_plugin.gd
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
class_name DMTranslationParserPlugin extends EditorTranslationParserPlugin
|
||||
|
||||
|
||||
## Cached result of parsing a dialogue file.
|
||||
var data: DMCompilerResult
|
||||
## List of characters that were added.
|
||||
var translated_character_names: PackedStringArray = []
|
||||
var translated_lines: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _parse_file(path: String) -> Array[PackedStringArray]:
|
||||
var msgs: Array[PackedStringArray] = []
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
||||
var text: String = file.get_as_text()
|
||||
|
||||
data = DMCompiler.compile_string(text, path)
|
||||
|
||||
var known_keys: PackedStringArray = PackedStringArray([])
|
||||
|
||||
# Add all character names if settings ask for it
|
||||
if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, true):
|
||||
translated_character_names = [] as Array[DialogueLine]
|
||||
for character_name: String in data.character_names:
|
||||
if character_name in known_keys: continue
|
||||
|
||||
known_keys.append(character_name)
|
||||
|
||||
translated_character_names.append(character_name)
|
||||
msgs.append(PackedStringArray([character_name.replace('"', '\"'), "dialogue", "", DMConstants.translate("translation_plugin.character_name")]))
|
||||
|
||||
# Add all dialogue lines and responses
|
||||
var dialogue: Dictionary = data.lines
|
||||
for key: String in dialogue.keys():
|
||||
var line: Dictionary = dialogue.get(key)
|
||||
|
||||
if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue
|
||||
|
||||
var translation_key: String = line.get(&"translation_key", line.text)
|
||||
|
||||
if translation_key in known_keys: continue
|
||||
|
||||
known_keys.append(translation_key)
|
||||
translated_lines.append(line)
|
||||
if translation_key == line.text:
|
||||
msgs.append(PackedStringArray([line.text.replace('"', '\"'), "", "", line.get("notes", "")]))
|
||||
else:
|
||||
msgs.append(PackedStringArray([line.text.replace('"', '\"'), line.translation_key.replace('"', '\"'), "", line.get("notes", "")]))
|
||||
|
||||
return msgs
|
||||
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return ["dialogue"]
|
@@ -0,0 +1 @@
|
||||
uid://c6bya881h1egb
|
223
addons/dialogue_manager/example_balloon/ExampleBalloon.cs
Normal file
223
addons/dialogue_manager/example_balloon/ExampleBalloon.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace DialogueManagerRuntime
|
||||
{
|
||||
public partial class ExampleBalloon : CanvasLayer
|
||||
{
|
||||
[Export] public string NextAction = "ui_accept";
|
||||
[Export] public string SkipAction = "ui_cancel";
|
||||
|
||||
|
||||
Control balloon;
|
||||
RichTextLabel characterLabel;
|
||||
RichTextLabel dialogueLabel;
|
||||
VBoxContainer responsesMenu;
|
||||
|
||||
Resource resource;
|
||||
Array<Variant> temporaryGameStates = new Array<Variant>();
|
||||
bool isWaitingForInput = false;
|
||||
bool willHideBalloon = false;
|
||||
|
||||
DialogueLine dialogueLine;
|
||||
DialogueLine DialogueLine
|
||||
{
|
||||
get => dialogueLine;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
QueueFree();
|
||||
return;
|
||||
}
|
||||
|
||||
dialogueLine = value;
|
||||
ApplyDialogueLine();
|
||||
}
|
||||
}
|
||||
|
||||
Timer MutationCooldown = new Timer();
|
||||
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
balloon = GetNode<Control>("%Balloon");
|
||||
characterLabel = GetNode<RichTextLabel>("%CharacterLabel");
|
||||
dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel");
|
||||
responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu");
|
||||
|
||||
balloon.Hide();
|
||||
|
||||
balloon.GuiInput += (@event) =>
|
||||
{
|
||||
if ((bool)dialogueLabel.Get("is_typing"))
|
||||
{
|
||||
bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed();
|
||||
bool skipButtonWasPressed = @event.IsActionPressed(SkipAction);
|
||||
if (mouseWasClicked || skipButtonWasPressed)
|
||||
{
|
||||
GetViewport().SetInputAsHandled();
|
||||
dialogueLabel.Call("skip_typing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isWaitingForInput) return;
|
||||
if (dialogueLine.Responses.Count > 0) return;
|
||||
|
||||
GetViewport().SetInputAsHandled();
|
||||
|
||||
if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
Next(dialogueLine.NextId);
|
||||
}
|
||||
else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon)
|
||||
{
|
||||
Next(dialogueLine.NextId);
|
||||
}
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action")))
|
||||
{
|
||||
responsesMenu.Set("next_action", NextAction);
|
||||
}
|
||||
responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) =>
|
||||
{
|
||||
Next(response.NextId);
|
||||
}));
|
||||
|
||||
|
||||
// Hide the balloon when a mutation is running
|
||||
MutationCooldown.Timeout += () =>
|
||||
{
|
||||
if (willHideBalloon)
|
||||
{
|
||||
willHideBalloon = false;
|
||||
balloon.Hide();
|
||||
}
|
||||
};
|
||||
AddChild(MutationCooldown);
|
||||
|
||||
DialogueManager.Mutated += OnMutated;
|
||||
}
|
||||
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
DialogueManager.Mutated -= OnMutated;
|
||||
}
|
||||
|
||||
|
||||
public override void _UnhandledInput(InputEvent @event)
|
||||
{
|
||||
// Only the balloon is allowed to handle input while it's showing
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
|
||||
|
||||
public override async void _Notification(int what)
|
||||
{
|
||||
// Detect a change of locale and update the current dialogue line to show the new language
|
||||
if (what == NotificationTranslationChanged && IsInstanceValid(dialogueLabel))
|
||||
{
|
||||
float visibleRatio = dialogueLabel.VisibleRatio;
|
||||
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, DialogueLine.Id, temporaryGameStates);
|
||||
if (visibleRatio < 1.0f)
|
||||
{
|
||||
dialogueLabel.Call("skip_typing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null)
|
||||
{
|
||||
temporaryGameStates = new Array<Variant> { this } + (extraGameStates ?? new Array<Variant>());
|
||||
isWaitingForInput = false;
|
||||
resource = dialogueResource;
|
||||
|
||||
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates);
|
||||
}
|
||||
|
||||
|
||||
public async void Next(string nextId)
|
||||
{
|
||||
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates);
|
||||
}
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
|
||||
private async void ApplyDialogueLine()
|
||||
{
|
||||
MutationCooldown.Stop();
|
||||
|
||||
isWaitingForInput = false;
|
||||
balloon.FocusMode = Control.FocusModeEnum.All;
|
||||
balloon.GrabFocus();
|
||||
|
||||
// Set up the character name
|
||||
characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character);
|
||||
characterLabel.Text = Tr(dialogueLine.Character, "dialogue");
|
||||
|
||||
// Set up the dialogue
|
||||
dialogueLabel.Hide();
|
||||
dialogueLabel.Set("dialogue_line", dialogueLine);
|
||||
|
||||
// Set up the responses
|
||||
responsesMenu.Hide();
|
||||
responsesMenu.Set("responses", dialogueLine.Responses);
|
||||
|
||||
// Type out the text
|
||||
balloon.Show();
|
||||
willHideBalloon = false;
|
||||
dialogueLabel.Show();
|
||||
if (!string.IsNullOrEmpty(dialogueLine.Text))
|
||||
{
|
||||
dialogueLabel.Call("type_out");
|
||||
await ToSignal(dialogueLabel, "finished_typing");
|
||||
}
|
||||
|
||||
// Wait for input
|
||||
if (dialogueLine.Responses.Count > 0)
|
||||
{
|
||||
balloon.FocusMode = Control.FocusModeEnum.None;
|
||||
responsesMenu.Show();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(dialogueLine.Time))
|
||||
{
|
||||
float time = 0f;
|
||||
if (!float.TryParse(dialogueLine.Time, out time))
|
||||
{
|
||||
time = dialogueLine.Text.Length * 0.02f;
|
||||
}
|
||||
await ToSignal(GetTree().CreateTimer(time), "timeout");
|
||||
Next(dialogueLine.NextId);
|
||||
}
|
||||
else
|
||||
{
|
||||
isWaitingForInput = true;
|
||||
balloon.FocusMode = Control.FocusModeEnum.All;
|
||||
balloon.GrabFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region signals
|
||||
|
||||
|
||||
private void OnMutated(Dictionary _mutation)
|
||||
{
|
||||
isWaitingForInput = false;
|
||||
willHideBalloon = true;
|
||||
MutationCooldown.Start(0.1f);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
uid://5b3w40kwakl3
|
176
addons/dialogue_manager/example_balloon/example_balloon.gd
vendored
Normal file
176
addons/dialogue_manager/example_balloon/example_balloon.gd
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
class_name DialogueManagerExampleBalloon extends CanvasLayer
|
||||
## A basic dialogue balloon for use with Dialogue Manager.
|
||||
|
||||
## The action to use for advancing the dialogue
|
||||
@export var next_action: StringName = &"ui_accept"
|
||||
|
||||
## The action to use to skip typing the dialogue
|
||||
@export var skip_action: StringName = &"ui_cancel"
|
||||
|
||||
## The dialogue resource
|
||||
var resource: DialogueResource
|
||||
|
||||
## Temporary game states
|
||||
var temporary_game_states: Array = []
|
||||
|
||||
## See if we are waiting for the player
|
||||
var is_waiting_for_input: bool = false
|
||||
|
||||
## See if we are running a long mutation and should hide the balloon
|
||||
var will_hide_balloon: bool = false
|
||||
|
||||
## A dictionary to store any ephemeral variables
|
||||
var locals: Dictionary = {}
|
||||
|
||||
var _locale: String = TranslationServer.get_locale()
|
||||
|
||||
## The current line
|
||||
var dialogue_line: DialogueLine:
|
||||
set(value):
|
||||
if value:
|
||||
dialogue_line = value
|
||||
apply_dialogue_line()
|
||||
else:
|
||||
# The dialogue has finished so close the balloon
|
||||
queue_free()
|
||||
get:
|
||||
return dialogue_line
|
||||
|
||||
## A cooldown timer for delaying the balloon hide when encountering a mutation.
|
||||
var mutation_cooldown: Timer = Timer.new()
|
||||
|
||||
## The base balloon anchor
|
||||
@onready var balloon: Control = %Balloon
|
||||
|
||||
## The label showing the name of the currently speaking character
|
||||
@onready var character_label: RichTextLabel = %CharacterLabel
|
||||
|
||||
## The label showing the currently spoken dialogue
|
||||
@onready var dialogue_label: DialogueLabel = %DialogueLabel
|
||||
|
||||
## The menu of responses
|
||||
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
balloon.hide()
|
||||
Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated)
|
||||
|
||||
# If the responses menu doesn't have a next action set, use this one
|
||||
if responses_menu.next_action.is_empty():
|
||||
responses_menu.next_action = next_action
|
||||
|
||||
mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout)
|
||||
add_child(mutation_cooldown)
|
||||
|
||||
|
||||
func _unhandled_input(_event: InputEvent) -> void:
|
||||
# Only the balloon is allowed to handle input while it's showing
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
## Detect a change of locale and update the current dialogue line to show the new language
|
||||
if what == NOTIFICATION_TRANSLATION_CHANGED and _locale != TranslationServer.get_locale() and is_instance_valid(dialogue_label):
|
||||
_locale = TranslationServer.get_locale()
|
||||
var visible_ratio = dialogue_label.visible_ratio
|
||||
self.dialogue_line = await resource.get_next_dialogue_line(dialogue_line.id)
|
||||
if visible_ratio < 1:
|
||||
dialogue_label.skip_typing()
|
||||
|
||||
|
||||
## Start some dialogue
|
||||
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
|
||||
temporary_game_states = [self] + extra_game_states
|
||||
is_waiting_for_input = false
|
||||
resource = dialogue_resource
|
||||
self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states)
|
||||
|
||||
|
||||
## Apply any changes to the balloon given a new [DialogueLine].
|
||||
func apply_dialogue_line() -> void:
|
||||
mutation_cooldown.stop()
|
||||
|
||||
is_waiting_for_input = false
|
||||
balloon.focus_mode = Control.FOCUS_ALL
|
||||
balloon.grab_focus()
|
||||
|
||||
character_label.visible = not dialogue_line.character.is_empty()
|
||||
character_label.text = tr(dialogue_line.character, "dialogue")
|
||||
|
||||
dialogue_label.hide()
|
||||
dialogue_label.dialogue_line = dialogue_line
|
||||
|
||||
responses_menu.hide()
|
||||
responses_menu.responses = dialogue_line.responses
|
||||
|
||||
# Show our balloon
|
||||
balloon.show()
|
||||
will_hide_balloon = false
|
||||
|
||||
dialogue_label.show()
|
||||
if not dialogue_line.text.is_empty():
|
||||
dialogue_label.type_out()
|
||||
await dialogue_label.finished_typing
|
||||
|
||||
# Wait for input
|
||||
if dialogue_line.responses.size() > 0:
|
||||
balloon.focus_mode = Control.FOCUS_NONE
|
||||
responses_menu.show()
|
||||
elif dialogue_line.time != "":
|
||||
var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float()
|
||||
await get_tree().create_timer(time).timeout
|
||||
next(dialogue_line.next_id)
|
||||
else:
|
||||
is_waiting_for_input = true
|
||||
balloon.focus_mode = Control.FOCUS_ALL
|
||||
balloon.grab_focus()
|
||||
|
||||
|
||||
## Go to the next line
|
||||
func next(next_id: String) -> void:
|
||||
self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states)
|
||||
|
||||
|
||||
#region Signals
|
||||
|
||||
|
||||
func _on_mutation_cooldown_timeout() -> void:
|
||||
if will_hide_balloon:
|
||||
will_hide_balloon = false
|
||||
balloon.hide()
|
||||
|
||||
|
||||
func _on_mutated(_mutation: Dictionary) -> void:
|
||||
is_waiting_for_input = false
|
||||
will_hide_balloon = true
|
||||
mutation_cooldown.start(0.1)
|
||||
|
||||
|
||||
func _on_balloon_gui_input(event: InputEvent) -> void:
|
||||
# See if we need to skip typing of the dialogue
|
||||
if dialogue_label.is_typing:
|
||||
var mouse_was_clicked: bool = event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed()
|
||||
var skip_button_was_pressed: bool = event.is_action_pressed(skip_action)
|
||||
if mouse_was_clicked or skip_button_was_pressed:
|
||||
get_viewport().set_input_as_handled()
|
||||
dialogue_label.skip_typing()
|
||||
return
|
||||
|
||||
if not is_waiting_for_input: return
|
||||
if dialogue_line.responses.size() > 0: return
|
||||
|
||||
# When there are no response options the balloon itself is the clickable thing
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
next(dialogue_line.next_id)
|
||||
elif event.is_action_pressed(next_action) and get_viewport().gui_get_focus_owner() == balloon:
|
||||
next(dialogue_line.next_id)
|
||||
|
||||
|
||||
func _on_responses_menu_response_selected(response: DialogueResponse) -> void:
|
||||
next(response.next_id)
|
||||
|
||||
|
||||
#endregion
|
@@ -0,0 +1 @@
|
||||
uid://d1wt4ma6055l8
|
142
addons/dialogue_manager/example_balloon/example_balloon.tscn
Normal file
142
addons/dialogue_manager/example_balloon/example_balloon.tscn
Normal file
@@ -0,0 +1,142 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://5b3w40kwakl3" path="res://addons/dialogue_manager/example_balloon/ExampleBalloon.cs" id="1_36de5"]
|
||||
[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"]
|
||||
[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_72ixx"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(0.329412, 0.329412, 0.329412, 1)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ri4m3"]
|
||||
bg_color = Color(0.121569, 0.121569, 0.121569, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0njw"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
border_color = Color(0.6, 0.6, 0.6, 1)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qkmqt"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 3
|
||||
border_width_top = 3
|
||||
border_width_right = 3
|
||||
border_width_bottom = 3
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[sub_resource type="Theme" id="Theme_qq3yp"]
|
||||
default_font_size = 20
|
||||
Button/styles/disabled = SubResource("StyleBoxFlat_spyqn")
|
||||
Button/styles/focus = SubResource("StyleBoxFlat_ri4m3")
|
||||
Button/styles/hover = SubResource("StyleBoxFlat_e0njw")
|
||||
Button/styles/normal = SubResource("StyleBoxFlat_e0njw")
|
||||
MarginContainer/constants/margin_bottom = 15
|
||||
MarginContainer/constants/margin_left = 30
|
||||
MarginContainer/constants/margin_right = 30
|
||||
MarginContainer/constants/margin_top = 15
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt")
|
||||
|
||||
[node name="ExampleBalloon" type="CanvasLayer"]
|
||||
layer = 100
|
||||
script = ExtResource("1_36de5")
|
||||
|
||||
[node name="Balloon" type="Control" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = SubResource("Theme_qq3yp")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -219.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"]
|
||||
clip_children = 2
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
modulate = Color(1, 1, 1, 0.501961)
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
text = "Character"
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
|
||||
[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_a8ve6")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
text = "Dialogue..."
|
||||
|
||||
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon" node_paths=PackedStringArray("response_template")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -290.5
|
||||
offset_top = -35.0
|
||||
offset_right = 290.5
|
||||
offset_bottom = 35.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 8
|
||||
theme_override_constants/separation = 2
|
||||
alignment = 1
|
||||
script = ExtResource("3_72ixx")
|
||||
response_template = NodePath("ResponseExample")
|
||||
|
||||
[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"]
|
||||
layout_mode = 2
|
||||
text = "Response example"
|
||||
|
||||
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
|
||||
[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]
|
@@ -0,0 +1,166 @@
|
||||
[gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://5b3w40kwakl3" path="res://addons/dialogue_manager/example_balloon/ExampleBalloon.cs" id="1_s2gbs"]
|
||||
[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"]
|
||||
[ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_1j1j0"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.345098, 0.345098, 0.345098, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ufjut"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.227451, 0.227451, 0.227451, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fcbqo"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t6i7a"]
|
||||
content_margin_left = 6.0
|
||||
content_margin_top = 3.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_bottom = 3.0
|
||||
bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="Theme" id="Theme_qq3yp"]
|
||||
default_font_size = 8
|
||||
Button/styles/disabled = SubResource("StyleBoxFlat_235ry")
|
||||
Button/styles/focus = SubResource("StyleBoxFlat_ufjut")
|
||||
Button/styles/hover = SubResource("StyleBoxFlat_fcbqo")
|
||||
Button/styles/normal = SubResource("StyleBoxFlat_t6i7a")
|
||||
MarginContainer/constants/margin_bottom = 4
|
||||
MarginContainer/constants/margin_left = 8
|
||||
MarginContainer/constants/margin_right = 8
|
||||
MarginContainer/constants/margin_top = 4
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm")
|
||||
|
||||
[node name="ExampleBalloon" type="CanvasLayer"]
|
||||
layer = 100
|
||||
script = ExtResource("1_s2gbs")
|
||||
|
||||
[node name="Balloon" type="Control" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = SubResource("Theme_qq3yp")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -71.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"]
|
||||
clip_children = 2
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
modulate = Color(1, 1, 1, 0.501961)
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
bbcode_enabled = true
|
||||
text = "Character"
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
|
||||
[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_hfvdi")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
text = "Dialogue..."
|
||||
|
||||
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -116.5
|
||||
offset_top = -9.0
|
||||
offset_right = 116.5
|
||||
offset_bottom = 9.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 8
|
||||
theme_override_constants/separation = 2
|
||||
script = ExtResource("3_1j1j0")
|
||||
|
||||
[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"]
|
||||
layout_mode = 2
|
||||
text = "Response Example"
|
||||
|
||||
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
|
||||
[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]
|
26
addons/dialogue_manager/export_plugin.gd
vendored
Normal file
26
addons/dialogue_manager/export_plugin.gd
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
class_name DMExportPlugin extends EditorExportPlugin
|
||||
|
||||
const IGNORED_PATHS = [
|
||||
"/assets",
|
||||
"/components",
|
||||
"/views",
|
||||
"inspector_plugin",
|
||||
"test_scene"
|
||||
]
|
||||
|
||||
|
||||
func _get_name() -> String:
|
||||
return "Dialogue Manager Export Plugin"
|
||||
|
||||
|
||||
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
|
||||
var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path()
|
||||
|
||||
# Ignore any editor stuff
|
||||
for ignored_path: String in IGNORED_PATHS:
|
||||
if path.begins_with(plugin_path + ignored_path):
|
||||
skip()
|
||||
|
||||
# Ignore C# stuff it not using dotnet
|
||||
if path.begins_with(plugin_path) and not DMSettings.check_for_dotnet_solution() and path.ends_with(".cs"):
|
||||
skip()
|
1
addons/dialogue_manager/export_plugin.gd.uid
Normal file
1
addons/dialogue_manager/export_plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://sa55ra11ji2q
|
110
addons/dialogue_manager/import_plugin.gd
vendored
Normal file
110
addons/dialogue_manager/import_plugin.gd
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
@tool
|
||||
class_name DMImportPlugin extends EditorImportPlugin
|
||||
|
||||
|
||||
signal compiled_resource(resource: Resource)
|
||||
|
||||
|
||||
const COMPILER_VERSION = 15
|
||||
|
||||
|
||||
func _get_importer_name() -> String:
|
||||
return "dialogue_manager"
|
||||
|
||||
|
||||
func _get_format_version() -> int:
|
||||
return COMPILER_VERSION
|
||||
|
||||
|
||||
func _get_visible_name() -> String:
|
||||
return "Dialogue"
|
||||
|
||||
|
||||
func _get_import_order() -> int:
|
||||
return -1000
|
||||
|
||||
|
||||
func _get_priority() -> float:
|
||||
return 1000.0
|
||||
|
||||
|
||||
func _get_resource_type():
|
||||
return "Resource"
|
||||
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return PackedStringArray(["dialogue"])
|
||||
|
||||
|
||||
func _get_save_extension():
|
||||
return "tres"
|
||||
|
||||
|
||||
func _get_preset_count() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
func _get_preset_name(preset_index: int) -> String:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
func _get_import_options(path: String, preset_index: int) -> Array:
|
||||
# When the options array is empty there is a misleading error on export
|
||||
# that actually means nothing so let's just have an invisible option.
|
||||
return [{
|
||||
name = "defaults",
|
||||
default_value = true
|
||||
}]
|
||||
|
||||
|
||||
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error:
|
||||
var cache = Engine.get_meta("DMCache")
|
||||
|
||||
# Get the raw file contents
|
||||
if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND
|
||||
|
||||
var file: FileAccess = FileAccess.open(source_file, FileAccess.READ)
|
||||
var raw_text: String = file.get_as_text()
|
||||
|
||||
cache.file_content_changed.emit(source_file, raw_text)
|
||||
|
||||
# Compile the text
|
||||
var result: DMCompilerResult = DMCompiler.compile_string(raw_text, source_file)
|
||||
if result.errors.size() > 0:
|
||||
printerr("%d errors found in %s" % [result.errors.size(), source_file])
|
||||
cache.add_errors_to_file(source_file, result.errors)
|
||||
return OK
|
||||
|
||||
# Get the current addon version
|
||||
var config: ConfigFile = ConfigFile.new()
|
||||
config.load("res://addons/dialogue_manager/plugin.cfg")
|
||||
var version: String = config.get_value("plugin", "version")
|
||||
|
||||
# Save the results to a resource
|
||||
var resource: DialogueResource = DialogueResource.new()
|
||||
resource.set_meta("dialogue_manager_version", version)
|
||||
|
||||
resource.using_states = result.using_states
|
||||
resource.titles = result.titles
|
||||
resource.first_title = result.first_title
|
||||
resource.character_names = result.character_names
|
||||
resource.lines = result.lines
|
||||
resource.raw_text = result.raw_text
|
||||
|
||||
# Clear errors and possibly trigger any cascade recompiles
|
||||
cache.add_file(source_file, result)
|
||||
|
||||
var err: Error = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()])
|
||||
|
||||
compiled_resource.emit(resource)
|
||||
|
||||
# Recompile any dependencies
|
||||
var dependent_paths: PackedStringArray = cache.get_dependent_paths_for_reimport(source_file)
|
||||
for path in dependent_paths:
|
||||
append_import_external_resource(path)
|
||||
|
||||
return err
|
1
addons/dialogue_manager/import_plugin.gd.uid
Normal file
1
addons/dialogue_manager/import_plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dhwpj6ed8soyq
|
21
addons/dialogue_manager/inspector_plugin.gd
vendored
Normal file
21
addons/dialogue_manager/inspector_plugin.gd
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
@tool
|
||||
class_name DMInspectorPlugin extends EditorInspectorPlugin
|
||||
|
||||
|
||||
const DialogueEditorProperty = preload("./components/editor_property/editor_property.gd")
|
||||
|
||||
|
||||
func _can_handle(object) -> bool:
|
||||
if object is GDScript: return false
|
||||
if not object is Node: return false
|
||||
if "name" in object and object.name == "Dialogue Manager": return false
|
||||
return true
|
||||
|
||||
|
||||
func _parse_property(object: Object, type, name: String, hint_type, hint_string: String, usage_flags: int, wide: bool) -> bool:
|
||||
if hint_string == "DialogueResource" or ("dialogue" in name.to_lower() and hint_string == "Resource"):
|
||||
var property_editor = DialogueEditorProperty.new()
|
||||
add_property_editor(name, property_editor)
|
||||
return true
|
||||
|
||||
return false
|
1
addons/dialogue_manager/inspector_plugin.gd.uid
Normal file
1
addons/dialogue_manager/inspector_plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://0x31sbqbikov
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user