update dialogue manager

pull/36/head
HumanoidSandvichDispenser 2024-04-08 14:42:12 -07:00
parent e3d6fbb714
commit f07ce2f5df
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
52 changed files with 6262 additions and 4232 deletions

View File

@ -1,144 +1,394 @@
using Godot;
using Godot.Collections;
using System;
using System.Reflection;
using System.Threading.Tasks;
#nullable enable
namespace DialogueManagerRuntime
{
public partial class DialogueManager : Node
{
public static async Task<DialogueLine> GetNextDialogueLine(Resource dialogueResource, string key = "0", Array<Variant> extraGameStates = null)
public enum TranslationSource
{
var dialogueManager = Engine.GetSingleton("DialogueManager");
dialogueManager.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>());
var result = await dialogueManager.ToSignal(dialogueManager, "bridge_get_next_dialogue_line_completed");
None,
Guess,
CSV,
PO
}
if ((RefCounted)result[0] == null) return null;
public partial class DialogueManager : Node
{
public delegate void PassedTitleEventHandler(string title);
public delegate void GotDialogueEventHandler(DialogueLine dialogueLine);
public delegate void MutatedEventHandler(Dictionary mutation);
public delegate void DialogueEndedEventHandler(Resource dialogueResource);
return new DialogueLine((RefCounted)result[0]);
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");
}
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 void Prepare()
{
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 async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
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");
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 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 bool ThingHasMethod(GodotObject thing, string method)
{
MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);
return info != null;
}
public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> args)
{
MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);
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++)
{
if (i < args.Count && args[i].Obj != null)
{
_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);
if (info.ReturnType == typeof(Task))
{
await (Task)info.Invoke(thing, _args);
EmitSignal(SignalName.Resolved, null);
}
else
{
var value = (Variant)info.Invoke(thing, _args);
EmitSignal(SignalName.Resolved, value);
}
}
#nullable enable
}
public static void ShowExampleDialogueBalloon(Resource dialogueResource, string key = "0", Array<Variant> extraGameStates = null)
public partial class DialogueLine : RefCounted
{
Engine.GetSingleton("DialogueManager").Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
}
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<Variant> extra_game_states = new Array<Variant>();
private Array<string> tags = new Array<string>();
public Array<string> Tags
{
get => tags;
}
public DialogueLine(RefCounted data)
{
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 response in (Array<RefCounted>)data.Get("responses"))
{
responses.Add(new DialogueResponse(response));
}
}
public partial class DialogueLine : RefCounted
{
private string type = "dialogue";
public string Type
{
get => type;
set => type = value;
}
public string GetTagValue(string tagName)
{
string wrapped = $"{tagName}=";
foreach (var tag in tags)
{
if (tag.StartsWith(wrapped))
{
return tag.Substring(wrapped.Length);
}
}
return "";
}
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();
private Dictionary speeds = new Dictionary();
private Array<Array> inline_mutations = new Array<Array>();
private Array<Variant> extra_game_states = new Array<Variant>();
public DialogueLine(RefCounted data)
{
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<Array>)data.Get("inline_mutations");
foreach (var response in (Array<RefCounted>)data.Get("responses"))
{
responses.Add(new DialogueResponse(response));
}
}
}
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 text = "";
public string Text
{
get => text;
set => text = value;
}
private string translation_key = "";
public string TranslationKey
{
get => translation_key;
set => translation_key = value;
public override string ToString()
{
switch (type)
{
case "dialogue":
return $"<DialogueLine character=\"{character}\" text=\"{text}\">";
case "mutation":
return "<DialogueLine mutation>";
default:
return "";
}
}
}
public DialogueResponse(RefCounted data)
public partial class DialogueResponse : RefCounted
{
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");
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 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}\"";
}
}
}
}
}

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022-present Nathan Hoad
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

View 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

View 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

View File

@ -7,6 +7,9 @@ signal error_clicked(line_number: int)
signal external_file_requested(path: String, title: String)
const DialogueSyntaxHighlighter = preload("./code_edit_syntax_highlighter.gd")
# A link back to the owner MainView
var main_view
@ -15,50 +18,9 @@ var theme_overrides: Dictionary:
set(value):
theme_overrides = value
syntax_highlighter.clear_color_regions()
syntax_highlighter.clear_keyword_colors()
# Imports
syntax_highlighter.add_keyword_color("import", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("as", theme_overrides.conditions_color)
# Titles
syntax_highlighter.add_color_region("~", "~", theme_overrides.titles_color, true)
# Comments
syntax_highlighter.add_color_region("#", "##", theme_overrides.comments_color, true)
# Conditions
syntax_highlighter.add_keyword_color("if", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("elif", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("else", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("while", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("endif", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("in", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("and", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("or", theme_overrides.conditions_color)
syntax_highlighter.add_keyword_color("not", theme_overrides.conditions_color)
# Values
syntax_highlighter.add_keyword_color("true", theme_overrides.numbers_color)
syntax_highlighter.add_keyword_color("false", theme_overrides.numbers_color)
syntax_highlighter.number_color = theme_overrides.numbers_color
syntax_highlighter.add_color_region("\"", "\"", theme_overrides.strings_color)
# Mutations
syntax_highlighter.add_keyword_color("do", theme_overrides.mutations_color)
syntax_highlighter.add_keyword_color("set", theme_overrides.mutations_color)
syntax_highlighter.function_color = theme_overrides.mutations_color
syntax_highlighter.member_variable_color = theme_overrides.members_color
# Jumps
syntax_highlighter.add_color_region("=>", "<=", theme_overrides.jumps_color, true)
# Dialogue
syntax_highlighter.add_color_region(": ", "::", theme_overrides.text_color, true)
syntax_highlighter = DialogueSyntaxHighlighter.new()
# General UI
syntax_highlighter.symbol_color = theme_overrides.symbols_color
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)
@ -92,6 +54,8 @@ var font_size: int:
get:
return font_size
var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s")
func _ready() -> void:
# Add error gutter
@ -102,6 +66,8 @@ func _ready() -> void:
if not has_comment_delimiter("#"):
add_comment_delimiter("#", "", true)
syntax_highlighter = DialogueSyntaxHighlighter.new()
func _gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed():
@ -192,8 +158,7 @@ func _request_code_completion(force: bool) -> void:
parser.free()
return
# var last_character: String = current_line.substr(cursor.x - 1, 1)
var name_so_far: String = current_line.strip_edges()
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)
@ -260,10 +225,10 @@ func check_active_title() -> void:
# 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("~ "):
emit_signal("active_title_change", lines[i].replace("~ ", ""))
active_title_change.emit(lines[i].replace("~ ", ""))
return
emit_signal("active_title_change", "0")
active_title_change.emit("")
# Move the caret line to match a given title
@ -280,7 +245,7 @@ func get_character_names(beginning_with: String) -> PackedStringArray:
var lines = text.split("\n")
for line in lines:
if ": " in line:
var name: String = line.split(": ")[0].strip_edges()
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
@ -320,47 +285,68 @@ func insert_text(text: String) -> void:
# Toggle the selected lines as comments
func toggle_comment() -> void:
# Starting complex operation so that the entire toggle comment can be undone in a single step
begin_complex_operation()
var caret_count: int = get_caret_count()
var caret_offsets: Dictionary = {}
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 caret_count:
var caret_line: int = get_caret_line(caret_index)
var from: int = caret_line
var to: int = caret_line
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 = get_selection_from_line(caret_index)
to = get_selection_to_line(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)
for line in range(from, to + 1):
if line not in caret_offsets:
caret_offsets[line] = 0
selections.append({
from_line = from_line,
from_column = from_column,
to_line = to_line,
to_column = to_column
})
var line_text: String = get_line(line)
var comment_delimiter: String = delimiter_comments[0]
var is_line_commented: bool = line_text.begins_with(comment_delimiter)
set_line(line, line_text.substr(comment_delimiter.length()) if is_line_commented else comment_delimiter + line_text)
caret_offsets[line] += (-1 if is_line_commented else 1) * comment_delimiter.length()
for line_number in range(from_line, to_line + 1):
if line_offsets.has(line_number): continue
emit_signal("lines_edited_from", from, to)
var line_text: String = get_line(line_number)
# Readjust carets and selection positions after all carets effect have been calculated
# Tried making it in the above loop, but that causes a weird behaviour if two carets are on the same line (first caret will move, but not the second one)
for caret_index in caret_count:
if has_selection(caret_index):
var from: int = get_selection_from_line(caret_index)
var to: int = get_selection_to_line(caret_index)
select(from, get_selection_from_column(caret_index) + caret_offsets[from], to, get_selection_to_column(caret_index) + caret_offsets[to], caret_index)
# 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)
set_caret_column(get_caret_column(caret_index) + caret_offsets[get_caret_line(caret_index)], true, caret_index)
# 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()
emit_signal("text_set")
emit_signal("text_changed")
text_set.emit()
text_changed.emit()
# Move the selected lines up or down
@ -395,7 +381,7 @@ func move_line(offset: int) -> void:
if reselect:
select(from, 0, to, get_line_width(to))
set_cursor(cursor)
emit_signal("text_changed")
text_changed.emit()
### Signals
@ -415,7 +401,7 @@ func _on_code_edit_symbol_validate(symbol: String) -> void:
func _on_code_edit_symbol_lookup(symbol: String, line: int, column: int) -> void:
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
emit_signal("external_file_requested", symbol, "")
external_file_requested.emit(symbol, "")
else:
go_to_title(symbol)
@ -436,4 +422,4 @@ func _on_code_edit_caret_changed() -> void:
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:
emit_signal("error_clicked", line)
error_clicked.emit(line)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,382 @@
@tool
extends SyntaxHighlighter
enum ExpressionType {DO, SET, IF}
var dialogue_manager_parser: DialogueManagerParser = DialogueManagerParser.new()
var regex_titles: RegEx = RegEx.create_from_string("^\\s*(?<title>~\\s+[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)")
var regex_comments: RegEx = RegEx.create_from_string("(?:(?>\"(?:\\\\\"|[^\"\\n])*\")[^\"\\n]*?\\s*(?<comment>#[^\\n]*)$|^[^\"#\\n]*?\\s*(?<comment2>#[^\\n]*))")
var regex_mutation: RegEx = RegEx.create_from_string("^\\s*(do|do!|set) (?<mutation>.*)")
var regex_condition: RegEx = RegEx.create_from_string("^\\s*(if|elif|while|else if) (?<condition>.*)")
var regex_wcondition: RegEx = RegEx.create_from_string("\\[if (?<condition>((?:[^\\[\\]]*)|(?:\\[(?1)\\]))*?)\\]")
var regex_wendif: RegEx = RegEx.create_from_string("\\[(\\/if|else)\\]")
var regex_rgroup: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
var regex_endconditions: RegEx = RegEx.create_from_string("^\\s*(endif|else):?\\s*$")
var regex_tags: RegEx = RegEx.create_from_string("\\[(?<tag>(?!(?:ID:.*)|if)[a-zA-Z_][a-zA-Z0-9_]*!?)(?:[= ](?<val>[^\\[\\]]+))?\\](?:(?<text>(?!\\[\\/\\k<tag>\\]).*?)?(?<end>\\[\\/\\k<tag>\\]))?")
var regex_dialogue: RegEx = RegEx.create_from_string("^\\s*(?:(?<random>\\%[\\d.]* )|(?<response>- ))?(?:(?<character>[^#:]*): )?(?<dialogue>.*)$")
var regex_goto: RegEx = RegEx.create_from_string("=><? (?:(?<file>[^\\/]+)\\/)?(?<title>[^\\/]*)")
var regex_string: RegEx = RegEx.create_from_string("^(?<delimiter>[\"'])(?<content>(?:\\\\{2})*|(?:.*?[^\\\\](?:\\\\{2})*))\\1$")
var regex_escape: RegEx = RegEx.create_from_string("\\\\.")
var regex_number: RegEx = RegEx.create_from_string("^-?(?:(?:0x(?:[0-9A-Fa-f]{2})+)|(?:0b[01]+)|(?:\\d+(?:(?:[\\.]\\d*)?(?:e\\d+)?)|(?:_\\d+)+)?)$")
var regex_array: RegEx = RegEx.create_from_string("\\[((?>[^\\[\\]]+|(?R))*)\\]")
var regex_dict: RegEx = RegEx.create_from_string("^\\{((?>[^\\{\\}]+|(?R))*)\\}$")
var regex_kvdict: RegEx = RegEx.create_from_string("^\\s*(?<left>.*?)\\s*(?<colon>:|=)\\s*(?<right>[^\\/]+)$")
var regex_commas: RegEx = RegEx.create_from_string("([^,]+)(?:\\s*,\\s*)?")
var regex_assignment: RegEx = RegEx.create_from_string("^\\s*(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*(?<op>(?:\\/|\\*|-|\\+)?=)\\s*(?<val>.*)$")
var regex_varname: RegEx = RegEx.create_from_string("^\\s*(?!true|false|and|or|not|in|null)(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*$")
var regex_keyword: RegEx = RegEx.create_from_string("^\\s*(true|false|null)\\s*$")
var regex_function: RegEx = RegEx.create_from_string("^\\s*([a-zA-Z_][a-zA-Z_0-9]*\\s*)\\(")
var regex_comparison: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s*(?<op>==|>=|<=|<|>|!=)\\s*(?<right>.*)$")
var regex_blogical: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s+(?<op>and|or|in)\\s+(?<right>.*)$")
var regex_ulogical: RegEx = RegEx.create_from_string("^\\s*(?<op>not)\\s+(?<right>.*)$")
var regex_paren: RegEx = RegEx.create_from_string("\\((?<paren>((?:[^\\(\\)]*)|(?:\\((?1)\\)))*?)\\)")
var cache: Dictionary = {}
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
dialogue_manager_parser.free()
func _clear_highlighting_cache() -> void:
cache = {}
## Returns the syntax coloring for a dialogue file line
func _get_line_syntax_highlighting(line: int) -> Dictionary:
var colors: Dictionary = {}
var text_edit: TextEdit = get_text_edit()
var text: String = text_edit.get_line(line)
# Prevents 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]
# Comments, we have to remove them at this point so the rest of the processing is easier
# Counts both end-of-line and single-line comments
# Comments are not allowed within dialogue lines or response lines, so we ask the parser what it thinks the current line is
if not (dialogue_manager_parser.is_dialogue_line(text) or dialogue_manager_parser.is_response_line(text)) or dialogue_manager_parser.is_line_empty(text) or dialogue_manager_parser.is_import_line(text):
var comment_matches: Array[RegExMatch] = regex_comments.search_all(text)
for comment_match in comment_matches:
for i in ["comment", "comment2"]:
if i in comment_match.names:
colors[comment_match.get_start(i)] = {"color": text_edit.theme_overrides.comments_color}
text = text.substr(0, comment_match.get_start(i))
# Dialogues.
var dialogue_matches: Array[RegExMatch] = regex_dialogue.search_all(text)
for dialogue_match in dialogue_matches:
if "random" in dialogue_match.names:
colors[dialogue_match.get_start("random")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("random")] = {"color": text_edit.theme_overrides.text_color}
if "response" in dialogue_match.names:
colors[dialogue_match.get_start("response")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("response")] = {"color": text_edit.theme_overrides.text_color}
if "character" in dialogue_match.names:
colors[dialogue_match.get_start("character")] = {"color": text_edit.theme_overrides.members_color}
colors[dialogue_match.get_end("character")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_dialogue_syntax_highlighting(dialogue_match.get_start("dialogue"), dialogue_match.get_string("dialogue")), true)
# Title lines.
if dialogue_manager_parser.is_title_line(text):
var title_matches: Array[RegExMatch] = regex_titles.search_all(text)
for title_match in title_matches:
colors[title_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color}
# Import lines.
var import_matches: Array[RegExMatch] = dialogue_manager_parser.IMPORT_REGEX.search_all(text)
for import_match in import_matches:
colors[import_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[import_match.get_start("path") - 1] = {"color": text_edit.theme_overrides.strings_color}
colors[import_match.get_end("path") + 1] = {"color": text_edit.theme_overrides.conditions_color}
colors[import_match.get_start("prefix")] = {"color": text_edit.theme_overrides.members_color}
colors[import_match.get_end("prefix")] = {"color": text_edit.theme_overrides.conditions_color}
# Using clauses
var using_matches: Array[RegExMatch] = dialogue_manager_parser.USING_REGEX.search_all(text)
for using_match in using_matches:
colors[using_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[using_match.get_start("state") - 1] = {"color": text_edit.theme_overrides.text_color}
# Condition keywords and expressions.
var condition_matches: Array[RegExMatch] = regex_condition.search_all(text)
for condition_match in condition_matches:
colors[condition_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[condition_match.get_end(1)] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_expression_syntax_highlighting(condition_match.get_start("condition"), ExpressionType.IF, condition_match.get_string("condition")), true)
# endif/else
var endcondition_matches: Array[RegExMatch] = regex_endconditions.search_all(text)
for endcondition_match in endcondition_matches:
colors[endcondition_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[endcondition_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Mutations.
var mutation_matches: Array[RegExMatch] = regex_mutation.search_all(text)
for mutation_match in mutation_matches:
colors[mutation_match.get_start(0)] = {"color": text_edit.theme_overrides.mutations_color}
colors.merge(_get_expression_syntax_highlighting(mutation_match.get_start("mutation"), ExpressionType.DO if mutation_match.strings[1] == "do" else ExpressionType.SET, mutation_match.get_string("mutation")), true)
# CodeEdit seems to have issues if the Dictionary keys weren't added in order?
var new_colors: Dictionary = {}
var ordered_keys: Array = colors.keys()
ordered_keys.sort()
for index in ordered_keys:
new_colors[index] = colors[index]
cache[text] = new_colors
return new_colors
## Returns the syntax highlighting for a dialogue line
func _get_dialogue_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# #tag style tags
var hashtag_matches: Array[RegExMatch] = dialogue_manager_parser.TAGS_REGEX.search_all(text)
for hashtag_match in hashtag_matches:
colors[start_index + hashtag_match.get_start(0)] = { "color": text_edit.theme_overrides.comments_color }
colors[start_index + hashtag_match.get_end(0)] = { "color": text_edit.theme_overrides.text_color }
# Global tags, like bbcode.
var tag_matches: Array[RegExMatch] = regex_tags.search_all(text)
for tag_match in tag_matches:
colors[start_index + tag_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
if "val" in tag_match.names:
colors.merge(_get_literal_syntax_highlighting(start_index + tag_match.get_start("val"), tag_match.get_string("val")), true)
colors[start_index + tag_match.get_end("val")] = {"color": text_edit.theme_overrides.symbols_color}
# Showing the text color straight in the editor for better ease-of-use
if tag_match.get_string("tag") == "color":
colors[start_index + tag_match.get_start("val")] = {"color": Color.from_string(tag_match.get_string("val"), text_edit.theme_overrides.text_color)}
if "text" in tag_match.names:
colors[start_index + tag_match.get_start("text")] = {"color": text_edit.theme_overrides.text_color}
# Text can still contain tags if several effects are applied ([center][b]Something[/b][/center], so recursing
colors.merge(_get_dialogue_syntax_highlighting(start_index + tag_match.get_start("text"), tag_match.get_string("text")), true)
colors[start_index + tag_match.get_end("text")] = {"color": text_edit.theme_overrides.symbols_color}
if "end" in tag_match.names:
colors[start_index + tag_match.get_start("end")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + tag_match.get_end("end")] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + tag_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# ID tag.
var translation_matches: Array[RegExMatch] = dialogue_manager_parser.TRANSLATION_REGEX.search_all(text)
for translation_match in translation_matches:
colors[start_index + translation_match.get_start(0)] = {"color": text_edit.theme_overrides.comments_color}
colors[start_index + translation_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Replacements.
var replacement_matches: Array[RegExMatch] = dialogue_manager_parser.REPLACEMENTS_REGEX.search_all(text)
for replacement_match in replacement_matches:
colors[start_index + replacement_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + replacement_match.get_start(1)] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + replacement_match.get_start(1), replacement_match.strings[1]), true)
colors[start_index + replacement_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + replacement_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Jump at the end of a response.
var goto_matches: Array[RegExMatch] = regex_goto.search_all(text)
for goto_match in goto_matches:
colors[start_index + goto_match.get_start(0)] = {"color": text_edit.theme_overrides.jumps_color}
if "file" in goto_match.names:
colors[start_index + goto_match.get_start("file")] = {"color": text_edit.theme_overrides.members_color}
colors[start_index + goto_match.get_end("file")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + goto_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color}
colors[start_index + goto_match.get_end("title")] = {"color": text_edit.theme_overrides.jumps_color}
colors[start_index + goto_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Wrapped condition.
var wcondition_matches: Array[RegExMatch] = regex_wcondition.search_all(text)
for wcondition_match in wcondition_matches:
colors[start_index + wcondition_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wcondition_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + wcondition_match.get_start(0) + 3] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + wcondition_match.get_start("condition"), wcondition_match.get_string("condition")), true)
colors[start_index + wcondition_match.get_end("condition")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wcondition_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# [/if] tag for color matching with the opening tag
var wendif_matches: Array[RegExMatch] = regex_wendif.search_all(text)
for wendif_match in wendif_matches:
colors[start_index + wendif_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + wendif_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Random groups
var rgroup_matches: Array[RegExMatch] = regex_rgroup.search_all(text)
for rgroup_match in rgroup_matches:
colors[start_index + rgroup_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options")] = {"color": text_edit.theme_overrides.text_color}
var separator_matches: Array[RegExMatch] = RegEx.create_from_string("\\|").search_all(rgroup_match.get_string("options"))
for separator_match in separator_matches:
colors[start_index + rgroup_match.get_start("options") + separator_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options") + separator_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + rgroup_match.get_end("options")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
return colors
## Returns the syntax highlighting for an expression (mutation set/do, or condition)
func _get_expression_syntax_highlighting(start_index: int, type: ExpressionType, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
if type == ExpressionType.SET:
var assignment_matches: Array[RegExMatch] = regex_assignment.search_all(text)
for assignment_match in assignment_matches:
colors[start_index + assignment_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color}
if "attr" in assignment_match.names:
colors[start_index + assignment_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color}
colors[start_index + assignment_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color}
if "key" in assignment_match.names:
# Braces are outside of the key, so coloring them symbols_color
colors[start_index + assignment_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("key"), assignment_match.get_string("key")), true)
colors[start_index + assignment_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + assignment_match.get_end("key") + 1] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + assignment_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + assignment_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("val"), assignment_match.get_string("val")), true)
else:
colors.merge(_get_literal_syntax_highlighting(start_index, text), true)
return colors
## Returns the syntax highlighting for a literal.
## For this purpose, "literal" refers to a regular code line that could be used to get a value out of:
## - function calls
## - real literals (bool, string, int, float, etc.)
## - logical operators (>, <, >=, or, and, not, etc.)
func _get_literal_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# Removing spaces at start/end of the literal
var text_length: int = text.length()
text = text.lstrip(" ")
start_index += text_length - text.length()
text = text.rstrip(" ")
# Parenthesis expression.
var paren_matches: Array[RegExMatch] = regex_paren.search_all(text)
for paren_match in paren_matches:
colors[start_index + paren_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + paren_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + paren_match.get_start("paren"), paren_match.get_string("paren")), true)
colors[start_index + paren_match.get_end(0) - 1] = {"color": text_edit.theme_overrides.symbols_color}
# Strings.
var string_matches: Array[RegExMatch] = regex_string.search_all(text)
for string_match in string_matches:
colors[start_index + string_match.get_start(0)] = {"color": text_edit.theme_overrides.strings_color}
if "content" in string_match.names:
var escape_matches: Array[RegExMatch] = regex_escape.search_all(string_match.get_string("content"))
for escape_match in escape_matches:
colors[start_index + string_match.get_start("content") + escape_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + string_match.get_start("content") + escape_match.get_end(0)] = {"color": text_edit.theme_overrides.strings_color}
# Numbers.
var number_matches: Array[RegExMatch] = regex_number.search_all(text)
for number_match in number_matches:
colors[start_index + number_match.get_start(0)] = {"color": text_edit.theme_overrides.numbers_color}
# Arrays.
var array_matches: Array[RegExMatch] = regex_array.search_all(text)
for array_match in array_matches:
colors[start_index + array_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + array_match.get_start(1), array_match.strings[1]), true)
colors[start_index + array_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Dictionaries.
var dict_matches: Array[RegExMatch] = regex_dict.search_all(text)
for dict_match in dict_matches:
colors[start_index + dict_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + dict_match.get_start(1), dict_match.strings[1]), true)
colors[start_index + dict_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Dictionary key: value pairs
var kvdict_matches: Array[RegExMatch] = regex_kvdict.search_all(text)
for kvdict_match in kvdict_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("left"), kvdict_match.get_string("left")), true)
colors[start_index + kvdict_match.get_start("colon")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + kvdict_match.get_end("colon")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("right"), kvdict_match.get_string("right")), true)
# Booleans.
var bool_matches: Array[RegExMatch] = regex_keyword.search_all(text)
for bool_match in bool_matches:
colors[start_index + bool_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
# Functions.
var function_matches: Array[RegExMatch] = regex_function.search_all(text)
for function_match in function_matches:
var last_brace_index: int = text.rfind(")")
colors[start_index + function_match.get_start(1)] = {"color": text_edit.theme_overrides.mutations_color}
colors[start_index + function_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + function_match.get_end(0), text.substr(function_match.get_end(0), last_brace_index - function_match.get_end(0))), true)
colors[start_index + last_brace_index] = {"color": text_edit.theme_overrides.symbols_color}
# Variables.
var varname_matches: Array[RegExMatch] = regex_varname.search_all(text)
for varname_match in varname_matches:
colors[start_index + varname_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color}
if "attr" in varname_match.names:
colors[start_index + varname_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color}
colors[start_index + varname_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color}
if "key" in varname_match.names:
# Braces are outside of the key, so coloring them symbols_color
colors[start_index + varname_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_literal_syntax_highlighting(start_index + varname_match.get_start("key"), varname_match.get_string("key")), true)
colors[start_index + varname_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color}
# Comparison operators.
var comparison_matches: Array[RegExMatch] = regex_comparison.search_all(text)
for comparison_match in comparison_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("left"), comparison_match.get_string("left")), true)
colors[start_index + comparison_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + comparison_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
var right = comparison_match.get_string("right")
if right.ends_with(":"):
right = right.substr(0, right.length() - 1)
colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("right"), right), true)
colors[start_index + comparison_match.get_start("right") + right.length()] = { "color": text_edit.theme_overrides.symbols_color }
# Logical binary operators.
var blogical_matches: Array[RegExMatch] = regex_blogical.search_all(text)
for blogical_match in blogical_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("left"), blogical_match.get_string("left")), true)
colors[start_index + blogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + blogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("right"), blogical_match.get_string("right")), true)
# Logical unary operators.
var ulogical_matches: Array[RegExMatch] = regex_ulogical.search_all(text)
for ulogical_match in ulogical_matches:
colors[start_index + ulogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + ulogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + ulogical_match.get_start("right"), ulogical_match.get_string("right")), true)
return colors
## Returns the syntax coloring for a list of literals separated by commas
func _get_list_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# Comma-separated list of literals (for arrays and function arguments)
var element_matches: Array[RegExMatch] = regex_commas.search_all(text)
for element_match in element_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + element_match.get_start(1), element_match.strings[1]), true)
return colors

View File

@ -0,0 +1,160 @@
extends Node
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const DialogueManagerParseResult = preload("./parse_result.gd")
# Keeps track of errors and dependencies.
# {
# <dialogue file path> = {
# path = <dialogue file path>,
# dependencies = [<dialogue file path>, <dialogue file path>],
# errors = [<error>, <error>]
# }
# }
var _cache: Dictionary = {}
var _update_dependency_timer: Timer = Timer.new()
var _update_dependency_paths: PackedStringArray = []
func _ready() -> void:
add_child(_update_dependency_timer)
_update_dependency_timer.timeout.connect(_on_update_dependency_timeout)
_build_cache()
func reimport_files(files: PackedStringArray = []) -> void:
if files.is_empty(): files = get_files()
var file_system: EditorFileSystem = Engine.get_meta("DialogueManagerPlugin") \
.get_editor_interface() \
.get_resource_filesystem()
# NOTE: Godot 4.2rc1 has an issue with reimporting more than one
# file at a time so we do them one by one
for file in files:
file_system.reimport_files([file])
await get_tree().create_timer(0.2)
## Add a dialogue file to the cache.
func add_file(path: String, parse_results: DialogueManagerParseResult = null) -> void:
_cache[path] = {
path = path,
dependencies = [],
errors = []
}
if parse_results != null:
_cache[path].dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path)
_cache[path].parsed_at = Time.get_ticks_msec()
# If this is a fresh cache entry then we need to check for dependencies
if parse_results == null and not _update_dependency_paths.has(path):
queue_updating_dependencies(path)
## Get the file paths in the cache.
func get_files() -> PackedStringArray:
return _cache.keys()
## Remember any errors in a dialogue file.
func add_errors_to_file(path: String, errors: Array[Dictionary]) -> void:
if _cache.has(path):
_cache[path].errors = errors
else:
_cache[path] = {
path = path,
resource_path = "",
dependencies = [],
errors = errors
}
## Get a list of files that have errors in them.
func get_files_with_errors() -> Array[Dictionary]:
var files_with_errors: Array[Dictionary] = []
for dialogue_file in _cache.values():
if dialogue_file and dialogue_file.errors.size() > 0:
files_with_errors.append(dialogue_file)
return files_with_errors
## Queue a file to have it's dependencies checked
func queue_updating_dependencies(of_path: String) -> void:
_update_dependency_timer.stop()
if not _update_dependency_paths.has(of_path):
_update_dependency_paths.append(of_path)
_update_dependency_timer.start(0.5)
## Update any references to a file path that has moved
func move_file_path(from_path: String, to_path: String) -> void:
if not _cache.has(from_path): return
if to_path != "":
_cache[to_path] = _cache[from_path].duplicate()
_cache.erase(from_path)
## Get any dialogue files that import a given path.
func get_files_with_dependency(imported_path: String) -> Array:
return _cache.values().filter(func(d): return d.dependencies.has(imported_path))
## Get any paths that are dependent on a given path
func get_dependent_paths_for_reimport(on_path: String) -> PackedStringArray:
return get_files_with_dependency(on_path) \
.filter(func(d): return Time.get_ticks_msec() - d.get("parsed_at", 0) > 3000) \
.map(func(d): return d.path)
# Build the initial cache for dialogue files.
func _build_cache() -> void:
var current_files: PackedStringArray = _get_dialogue_files_in_filesystem()
for file in current_files:
add_file(file)
# Recursively find any dialogue files in a directory
func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray:
var files: PackedStringArray = []
if DirAccess.dir_exists_absolute(path):
var dir = DirAccess.open(path)
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
var file_path: String = (path + "/" + file_name).simplify_path()
if dir.current_is_dir():
if not file_name in [".godot", ".tmp"]:
files.append_array(_get_dialogue_files_in_filesystem(file_path))
elif file_name.get_extension() == "dialogue":
files.append(file_path)
file_name = dir.get_next()
return files
### Signals
func _on_update_dependency_timeout() -> void:
_update_dependency_timer.stop()
var import_regex: RegEx = RegEx.create_from_string("import \"(?<path>.*?)\"")
var file: FileAccess
var found_imports: Array[RegExMatch]
for path in _update_dependency_paths:
# Open the file and check for any "import" lines
file = FileAccess.open(path, FileAccess.READ)
found_imports = import_regex.search_all(file.get_as_text())
var dependencies: PackedStringArray = []
for found in found_imports:
dependencies.append(found.strings[found.names.path])
_cache[path].dependencies = dependencies
_update_dependency_paths.clear()

View File

@ -6,7 +6,7 @@ signal failed()
signal updated(updated_to_version: String)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("../constants.gd")
const TEMP_FILE_NAME = "user://temp.zip"
@ -19,14 +19,14 @@ const TEMP_FILE_NAME = "user://temp.zip"
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)
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")
$VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update")
$VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes")
### Signals
@ -34,28 +34,27 @@ func _ready() -> void:
func _on_download_button_pressed() -> void:
# Safeguard the actual dialogue manager repo from accidentally updating itself
if FileAccess.file_exists("res://examples/test_scenes/test_scene.gd"):
if FileAccess.file_exists("res://examples/test_scenes/test_scene.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")
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:
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()
if DirAccess.dir_exists_absolute("res://addons/dialogue_manager"):
DirAccess.remove_absolute("res://addons/dialogue_manager")
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager"))
var zip_reader: ZIPReader = ZIPReader.new()
zip_reader.open(TEMP_FILE_NAME)
@ -77,7 +76,7 @@ func _on_http_request_request_completed(result: int, response_code: int, headers
zip_reader.close()
DirAccess.remove_absolute(TEMP_FILE_NAME)
updated.emit(next_version_release.tag_name.substr(1))

View File

@ -46,14 +46,14 @@ layout_mode = 2
[node name="DownloadButton" type="Button" parent="VBox/Center"]
unique_name_in_owner = true
layout_mode = 2
text = "Download and install update"
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..."
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"]

View File

@ -5,7 +5,7 @@ extends HBoxContainer
signal error_pressed(line_number)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("../constants.gd")
@onready var error_button: Button = $ErrorButton
@ -33,7 +33,7 @@ var errors: Array = []:
func _ready() -> void:
apply_theme()
hide()
## Set up colors and icons
func apply_theme() -> void:
@ -57,9 +57,11 @@ func show_error() -> void:
hide()
else:
show()
count_label.text = DialogueConstants.translate("n_of_n").format({ index = error_index + 1, total = errors.size() })
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 + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number + 1, 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

View File

@ -5,13 +5,16 @@ 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("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("../constants.gd")
const MODIFIED_SUFFIX = "(*)"
@export var icon: Texture2D
@onready var filter_edit: LineEdit = $FilterEdit
@onready var list: ItemList = $List
@ -41,7 +44,7 @@ var filter: String:
func _ready() -> void:
apply_theme()
filter_edit.placeholder_text = DialogueConstants.translate("files_list.filter")
filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter")
func select_file(file: String) -> void:
@ -96,7 +99,8 @@ func apply_filter() -> void:
var nice_file = file_map[file]
if file in unsaved_files:
nice_file += MODIFIED_SUFFIX
list.add_item(nice_file)
var new_id := list.add_item(nice_file)
list.set_item_icon(new_id, icon)
select_file(current_file_path)
@ -127,6 +131,11 @@ func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index:
if mouse_button_index == MOUSE_BUTTON_RIGHT:
file_popup_menu_requested.emit(at_position)
if mouse_button_index == MOUSE_BUTTON_MIDDLE:
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
var file = file_map.find_key(item_text)
file_middle_clicked.emit(file)
func _on_list_item_activated(index: int) -> void:
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")

View File

@ -1,18 +1,19 @@
[gd_scene load_steps=4 format=3 uid="uid://dnufpcdrreva3"]
[gd_scene load_steps=5 format=3 uid="uid://dnufpcdrreva3"]
[ext_resource type="Script" 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"]
[sub_resource type="Image" id="Image_mirhx"]
[sub_resource type="Image" id="Image_h3jns"]
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),
"data": PackedByteArray(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, 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, 94, 94, 127, 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, 94, 94, 127, 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, 94, 94, 127, 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, 255, 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, 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, 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, 94, 94, 54, 255, 94, 94, 57, 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, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 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, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 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, 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, 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_wy68i"]
image = SubResource("Image_mirhx")
[sub_resource type="ImageTexture" id="ImageTexture_44sbr"]
image = SubResource("Image_h3jns")
[node name="FilesList" type="VBoxContainer"]
anchors_preset = 15
@ -21,12 +22,13 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
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
right_icon = SubResource("ImageTexture_wy68i")
right_icon = SubResource("ImageTexture_44sbr")
[node name="List" type="ItemList" parent="."]
layout_mode = 2

View 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.symbols_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("DialogueCache")
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

View File

@ -0,0 +1,139 @@
[gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"]
[ext_resource type="Script" 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"]

View File

@ -1,7 +1,10 @@
class_name DialogueManagerParseResult extends RefCounted
var imported_paths: PackedStringArray = []
var using_states: PackedStringArray = []
var titles: Dictionary = {}
var character_names: PackedStringArray = []
var first_title: String = ""
var lines: Dictionary = {}
var errors: Array[Dictionary] = []
var raw_text: String = ""

View File

@ -3,19 +3,27 @@
class_name DialogueManagerParser extends Object
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const ResolvedLineData = preload("./resolved_line_data.gd")
const ResolvedTagData = preload("./resolved_tag_data.gd")
const DialogueManagerParseResult = preload("./parse_result.gd")
var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?<path>[^\"]+)\" as (?<prefix>[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)")
var USING_REGEX: RegEx = RegEx.create_from_string("using (?<state>.*)")
var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+$")
var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d")
var TRANSLATION_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?<tr>.*?)\\]")
var MUTATION_REGEX: RegEx = RegEx.create_from_string("(do|set) (?<mutation>.*)")
var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?<tags>.*?)\\]")
var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?<keyword>do|do!|set) (?<mutation>.*)")
var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if) (?<condition>.*)")
var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.*)\\]")
var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}")
var GOTO_REGEX: RegEx = RegEx.create_from_string("=><? (?<jump_to_title>.*)")
var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+")
var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.+?)\\](?<body>.*?)\\[\\/if\\]")
var TOKEN_DEFINITIONS: Dictionary = {
DialogueConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_][a-zA-Z_0-9]*\\("),
@ -33,28 +41,30 @@ var TOKEN_DEFINITIONS: Dictionary = {
DialogueConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"),
DialogueConstants.TOKEN_COMMA: RegEx.create_from_string("^,"),
DialogueConstants.TOKEN_DOT: RegEx.create_from_string("^\\."),
DialogueConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"),
DialogueConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)"),
DialogueConstants.TOKEN_STRING: RegEx.create_from_string("^(\".*?\"|\'.*?\')"),
DialogueConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"),
DialogueConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or)( |$)"),
DialogueConstants.TOKEN_STRING: RegEx.create_from_string("^(\".*?\"|\'.*?\')"),
DialogueConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_][a-zA-Z_0-9]*"),
DialogueConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*")
DialogueConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"),
DialogueConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"),
DialogueConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)")
}
var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>\\d+)? ")
var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>[\\d.]+)? ")
var raw_lines: PackedStringArray = []
var parent_stack: Array[String] = []
var parsed_lines: Dictionary = {}
var imported_paths: PackedStringArray = []
var using_states: PackedStringArray = []
var titles: Dictionary = {}
var character_names: PackedStringArray = []
var first_title: String = ""
var errors: Array[Dictionary] = []
var raw_text: String = ""
var _imported_line_map: Array[Dictionary] = []
var _imported_line_map: Dictionary = {}
var _imported_line_count: int = 0
var while_loopbacks: Array[String] = []
@ -74,9 +84,9 @@ static func parse_string(string: String, path: String) -> DialogueManagerParseRe
## Extract bbcode and other markers from a string
static func extract_markers_from_string(string: String) -> Dictionary:
static func extract_markers_from_string(string: String) -> ResolvedLineData:
var parser: DialogueManagerParser = DialogueManagerParser.new()
var markers: Dictionary = parser.extract_markers(string)
var markers: ResolvedLineData = parser.extract_markers(string)
parser.free()
return markers
@ -85,25 +95,30 @@ static func extract_markers_from_string(string: String) -> Dictionary:
## Parse some raw dialogue text. Returns a dictionary containing parse results
func parse(text: String, path: String) -> Error:
prepare(text, path)
raw_text = text
# Parse all of the content
var known_translations = {}
# Get list of known autoloads
var autoload_names: PackedStringArray = get_autoload_names()
# Keep track of the last doc comment
var doc_comments: Array[String] = []
# Then parse all lines
for id in range(0, raw_lines.size()):
var raw_line: String = raw_lines[id]
var line: Dictionary = {
id = str(id),
next_id = DialogueConstants.ID_NULL
}
# Ignore empty lines and comments
if is_line_empty(raw_line): continue
# Work out if we are inside a conditional or option or if we just
# indented back out of one
var indent_size: int = get_indent(raw_line)
if indent_size < parent_stack.size():
if indent_size < parent_stack.size() and not is_line_empty(raw_line):
for _tab in range(0, parent_stack.size() - indent_size):
parent_stack.pop_back()
@ -121,12 +136,33 @@ func parse(text: String, path: String) -> Error:
line["translation_key"] = translation_key
raw_line = raw_line.replace("[ID:%s]" % translation_key, "")
## Check for each kind of line
# Check for each kind of line
# Start shortcuts
if raw_line.begins_with("using "):
var using_match: RegExMatch = USING_REGEX.search(raw_line)
if "state" in using_match.names:
var using_state: String = using_match.strings[using_match.names.state].strip_edges()
if not using_state in autoload_names:
add_error(id, 0, DialogueConstants.ERR_UNKNOWN_USING)
elif not using_state in using_states:
using_states.append(using_state)
continue
# Response
if is_response_line(raw_line):
elif is_response_line(raw_line):
# Add any doc notes
line["notes"] = "\n".join(doc_comments)
doc_comments = []
parent_stack.append(str(id))
line["type"] = DialogueConstants.TYPE_RESPONSE
# Extract any #tags
var tag_data: ResolvedTagData = extract_tags(raw_line)
line["tags"] = tag_data.tags
raw_line = tag_data.line_without_tags
if " [if " in raw_line:
line["condition"] = extract_condition(raw_line, true, indent_size)
if " =>" in raw_line:
@ -147,6 +183,8 @@ func parse(text: String, path: String) -> Error:
if goto_line.next_id in [DialogueConstants.ID_ERROR, DialogueConstants.ID_ERROR_INVALID_TITLE, DialogueConstants.ID_ERROR_TITLE_HAS_NO_BODY]:
line["next_id"] = goto_line.next_id
line["character"] = ""
line["character_replacements"] = [] as Array[Dictionary]
line["text"] = extract_response_prompt(raw_line)
var previous_response_id = find_previous_response_id(id)
@ -177,44 +215,35 @@ func parse(text: String, path: String) -> Error:
# If this response has a character name in it then it will automatically be
# injected as a line of dialogue if the player selects it
var l = line.text.replace("\\:", "!ESCAPED_COLON!")
if ": " in l:
var first_child: Dictionary = {
type = DialogueConstants.TYPE_DIALOGUE,
next_id = line.next_id,
next_id_after = line.next_id_after,
text_replacements = line.text_replacements,
translation_key = line.get("translation_key")
}
var bits = Array(l.strip_edges().split(": "))
first_child["character"] = bits.pop_front()
# You can use variables in the character's name
first_child["character_replacements"] = extract_dialogue_replacements(first_child.character, first_child.character.length() + 2 + indent_size)
for replacement in first_child.character_replacements:
if replacement.has("error"):
add_error(id, replacement.index, replacement.error)
first_child["text"] = ": ".join(bits).replace("!ESCAPED_COLON!", ":")
line["character"] = first_child.character.strip_edges()
if not line["character"] in character_names:
character_names.append(line["character"])
line["text"] = first_child.text.strip_edges()
if first_child.translation_key == null:
first_child["translation_key"] = first_child.text
parsed_lines[str(id) + ".2"] = first_child
line["next_id"] = str(id) + ".2"
var response_text: String = line.text.replace("\\:", "!ESCAPED_COLON!")
if ": " in response_text:
if DialogueSettings.get_setting("create_lines_for_responses_with_characters", true):
var first_child: Dictionary = {
type = DialogueConstants.TYPE_DIALOGUE,
next_id = line.next_id,
next_id_after = line.next_id_after,
text_replacements = line.text_replacements,
tags = line.tags,
translation_key = line.get("translation_key")
}
parse_response_character_and_text(id, response_text, first_child, indent_size, parsed_lines)
line["character"] = first_child.character
line["character_replacements"] = first_child.character_replacements
line["text"] = first_child.text
line["translation_key"] = first_child.translation_key
parsed_lines[str(id) + ".2"] = first_child
line["next_id"] = str(id) + ".2"
else:
parse_response_character_and_text(id, response_text, line, indent_size, parsed_lines)
else:
line["text"] = l.replace("!ESCAPED_COLON!", ":")
line["text"] = response_text.replace("!ESCAPED_COLON!", ":")
# Title
elif is_title_line(raw_line):
line["type"] = DialogueConstants.TYPE_TITLE
if not raw_lines[id].begins_with("~"):
add_error(id, indent_size + 2, DialogueConstants.ERR_NESTED_TITLE)
else:
line["type"] = DialogueConstants.TYPE_TITLE
line["text"] = extract_title(raw_line)
# Titles can't have numbers as the first letter (unless they are external titles which get replaced with hashes)
if id >= _imported_line_count and BEGINS_WITH_NUMBER_REGEX.search(line.text):
@ -255,6 +284,10 @@ func parse(text: String, path: String) -> Error:
# Goto
elif is_goto_line(raw_line):
line["type"] = DialogueConstants.TYPE_GOTO
if raw_line.begins_with("%"):
apply_weighted_random(id, raw_line, indent_size, line)
line["next_id"] = extract_goto(raw_line)
if is_goto_snippet_line(raw_line):
line["is_snippet"] = true
@ -262,14 +295,69 @@ func parse(text: String, path: String) -> Error:
else:
line["is_snippet"] = false
# Dialogue
# Nested dialogue
elif is_nested_dialogue_line(raw_line, parsed_lines, raw_lines, indent_size):
var parent_line: Dictionary = parsed_lines.values().back()
var parent_indent_size: int = get_indent(raw_lines[parent_line.id.to_int()])
var should_update_translation_key: bool = parent_line.translation_key == parent_line.text
var suffix: String = raw_line.strip_edges(true, false)
if suffix == "":
suffix = " "
parent_line["text"] += "\n" + suffix
parent_line["text_replacements"] = extract_dialogue_replacements(parent_line.text, parent_line.character.length() + 2 + parent_indent_size)
for replacement in parent_line.text_replacements:
if replacement.has("error"):
add_error(id, replacement.index, replacement.error)
if should_update_translation_key:
parent_line["translation_key"] = parent_line.text
parent_line["next_id"] = get_line_after_line(id, parent_indent_size, parent_line)
# Ignore this line when checking for indent errors
remove_error(parent_line.id.to_int(), DialogueConstants.ERR_INVALID_INDENTATION)
var next_line = raw_lines[parent_line.next_id.to_int()]
if not is_dialogue_line(next_line) and get_indent(next_line) >= indent_size:
add_error(parent_line.next_id.to_int(), indent_size, DialogueConstants.ERR_INVALID_INDENTATION)
continue
elif raw_line.strip_edges().begins_with("##"):
doc_comments.append(raw_line.replace("##", "").strip_edges())
continue
elif is_line_empty(raw_line) or is_import_line(raw_line):
continue
# Regular dialogue
else:
# Remove escape character
if raw_line.begins_with("\\using"): raw_line = raw_line.substr(1)
if raw_line.begins_with("\\if"): raw_line = raw_line.substr(1)
if raw_line.begins_with("\\elif"): raw_line = raw_line.substr(1)
if raw_line.begins_with("\\else"): raw_line = raw_line.substr(1)
if raw_line.begins_with("\\while"): raw_line = raw_line.substr(1)
if raw_line.begins_with("\\-"): raw_line = raw_line.substr(1)
if raw_line.begins_with("\\~"): raw_line = raw_line.substr(1)
if raw_line.begins_with("\\=>"): raw_line = raw_line.substr(1)
# Add any doc notes
line["notes"] = "\n".join(doc_comments)
doc_comments = []
# Work out any weighted random siblings
if raw_line.begins_with("%"):
apply_weighted_random(id, raw_line, indent_size, line)
raw_line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line, "")
line["type"] = DialogueConstants.TYPE_DIALOGUE
# Extract any tags before we process the line
var tag_data: ResolvedTagData = extract_tags(raw_line)
line["tags"] = tag_data.tags
raw_line = tag_data.line_without_tags
var l = raw_line.replace("\\:", "!ESCAPED_COLON!")
if ": " in l:
var bits = Array(l.strip_edges().split(": "))
@ -336,7 +424,12 @@ func parse(text: String, path: String) -> Error:
add_error(id, indent_size, DialogueConstants.ERR_INVALID_CONDITION_INDENTATION)
# Line after normal line is indented to the right
elif line.type in [DialogueConstants.TYPE_TITLE, DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_MUTATION, DialogueConstants.TYPE_GOTO] and is_valid_id(line.next_id):
elif line.type in [
DialogueConstants.TYPE_TITLE,
DialogueConstants.TYPE_DIALOGUE,
DialogueConstants.TYPE_MUTATION,
DialogueConstants.TYPE_GOTO
] and is_valid_id(line.next_id):
var next_line = raw_lines[line.next_id.to_int()]
if next_line != null and get_indent(next_line) > indent_size:
add_error(id, indent_size, DialogueConstants.ERR_INVALID_INDENTATION)
@ -369,10 +462,14 @@ func parse(text: String, path: String) -> Error:
else:
line["next_id"] = line["parent_id"]
# Done!
parsed_lines[str(id)] = line
# Assume the last line ends the dialogue
var last_line: Dictionary = parsed_lines.values()[parsed_lines.values().size() - 1]
if last_line.next_id == "":
last_line.next_id = DialogueConstants.ID_END
if errors.size() > 0:
return ERR_PARSE_ERROR
@ -382,10 +479,13 @@ func parse(text: String, path: String) -> Error:
func get_data() -> DialogueManagerParseResult:
var data: DialogueManagerParseResult = DialogueManagerParseResult.new()
data.imported_paths = imported_paths
data.using_states = using_states
data.titles = titles
data.character_names = character_names
data.first_title = first_title
data.lines = parsed_lines
data.errors = errors
data.raw_text = raw_text
return data
@ -393,11 +493,13 @@ func get_data() -> DialogueManagerParseResult:
func get_errors() -> Array[Dictionary]:
return errors
## Prepare the parser by collecting all lines and titles
func prepare(text: String, path: String, include_imported_titles_hashes: bool = true) -> void:
using_states = []
errors = []
imported_paths = []
_imported_line_map = []
_imported_line_map = {}
while_loopbacks = []
titles = {}
character_names = []
@ -415,31 +517,33 @@ func prepare(text: String, path: String, include_imported_titles_hashes: bool =
var line = raw_lines[id]
if is_import_line(line):
var import_data = extract_import_path_and_name(line)
var import_hash: int = import_data.path.hash()
if import_data.size() > 0:
# Make a map so we can refer compiled lines to where they were imported from
_imported_line_map.append({
hash = import_data.path.hash(),
imported_on_line_number = id,
from_line = 0,
to_line = 0
})
# Keep track of titles so we can add imported ones later
if str(import_data.path.hash()) in imported_titles.keys():
errors.append({ line_number = id, column_number = 0, error = DialogueConstants.ERR_FILE_ALREADY_IMPORTED })
if str(import_hash) in imported_titles.keys():
add_error(id, 0, DialogueConstants.ERR_FILE_ALREADY_IMPORTED)
if import_data.prefix in imported_titles.values():
errors.append({ line_number = id, column_number = 0, error = DialogueConstants.ERR_DUPLICATE_IMPORT_NAME })
imported_titles[str(import_data.path.hash())] = import_data.prefix
add_error(id, 0, DialogueConstants.ERR_DUPLICATE_IMPORT_NAME)
imported_titles[str(import_hash)] = import_data.prefix
# Import the file content
if not import_data.path.hash() in known_imports:
var error: Error = import_content(import_data.path, import_data.prefix, known_imports)
if not known_imports.has(import_hash):
var error: Error = import_content(import_data.path, import_data.prefix, _imported_line_map, known_imports)
if error != OK:
errors.append({ line_number = id, column_number = 0, error = error })
add_error(id, 0, error)
# Make a map so we can refer compiled lines to where they were imported from
if not _imported_line_map.has(import_hash):
_imported_line_map[import_hash] = {
hash = import_hash,
imported_on_line_number = id,
from_line = 0,
to_line = 0
}
var imported_content: String = ""
var cummulative_line_number: int = 0
for item in _imported_line_map:
for item in _imported_line_map.values():
item["from_line"] = cummulative_line_number
if known_imports.has(item.hash):
cummulative_line_number += known_imports[item.hash].split("\n").size()
@ -479,12 +583,14 @@ func prepare(text: String, path: String, include_imported_titles_hashes: bool =
func add_error(line_number: int, column_number: int, error: int) -> void:
# See if the error was in an imported file
for item in _imported_line_map:
for item in _imported_line_map.values():
if line_number < item.to_line:
errors.append({
line_number = item.imported_on_line_number,
column_number = 0,
error = DialogueConstants.ERR_ERRORS_IN_IMPORTED_FILE
error = DialogueConstants.ERR_ERRORS_IN_IMPORTED_FILE,
external_error = error,
external_line_number = line_number
})
return
@ -496,6 +602,16 @@ func add_error(line_number: int, column_number: int, error: int) -> void:
})
func remove_error(line_number: int, error: int) -> void:
for i in range(errors.size() - 1, -1, -1):
var err = errors[i]
var is_native_error = err.line_number == line_number - _imported_line_count and err.error == error
var is_external_error = err.get("external_line_number") == line_number and err.get("external_error") == error
if is_native_error or is_external_error:
errors.remove_at(i)
return
func is_import_line(line: String) -> bool:
return line.begins_with("import ") and " as " in line
@ -518,19 +634,32 @@ func is_while_condition_line(line: String) -> bool:
func is_mutation_line(line: String) -> bool:
line = line.strip_edges(true, false)
return line.begins_with("do ") or line.begins_with("set ")
return line.begins_with("do ") or line.begins_with("do! ") or line.begins_with("set ")
func is_goto_line(line: String) -> bool:
line = line.strip_edges(true, false)
line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(line, "")
return line.begins_with("=> ") or line.begins_with("=>< ")
func is_goto_snippet_line(line: String) -> bool:
return line.strip_edges().begins_with("=>< ")
line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(line.strip_edges(), "")
return line.begins_with("=>< ")
func is_nested_dialogue_line(raw_line: String, parsed_lines: Dictionary, raw_lines: PackedStringArray, indent_size: int) -> bool:
if parsed_lines.values().is_empty(): return false
if raw_line.strip_edges().begins_with("#"): return false
var parent_line: Dictionary = parsed_lines.values().back()
if parent_line.type != DialogueConstants.TYPE_DIALOGUE: return false
if get_indent(raw_lines[parent_line.id.to_int()]) >= indent_size: return false
return true
func is_dialogue_line(line: String) -> bool:
if line == null: return false
if is_response_line(line): return false
if is_title_line(line): return false
if is_condition_line(line, true): return false
@ -577,7 +706,11 @@ func get_line_after_line(id: int, indent_size: int, line: Dictionary) -> String:
func get_indent(line: String) -> int:
return line.count("\t", 0, line.find(line.strip_edges()))
var tabs: RegExMatch = INDENT_REGEX.search(line)
if tabs:
return tabs.get_string().length()
else:
return 0
func get_next_nonempty_line_id(line_number: int) -> String:
@ -611,34 +744,43 @@ func find_previous_response_id(line_number: int) -> String:
func apply_weighted_random(id: int, raw_line: String, indent_size: int, line: Dictionary) -> void:
var weight: int = 1
var weight: float = 1
var found = WEIGHTED_RANDOM_SIBLINGS_REGEX.search(raw_line)
if found and found.names.has("weight"):
weight = found.strings[found.names.weight].to_int()
weight = found.strings[found.names.weight].to_float()
# Look back up the list to find the first weighted random line in this group
var original_random_line: Dictionary = {}
for i in range(id, 0, -1):
# Ignore doc comment lines
if raw_lines[i].strip_edges().begins_with("##"):
continue
# Lines that aren't prefixed with the random token are a dead end
if not raw_lines[i].strip_edges().begins_with("%") or get_indent(raw_lines[i]) != indent_size:
break
# Make sure we group random dialogue and random lines separately
elif WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line.strip_edges(), "").begins_with("=") != WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_lines[i].strip_edges(), "").begins_with("="):
break
# Otherwise we've found the origin
elif parsed_lines.has(str(i)) and parsed_lines[str(i)].has("siblings"):
original_random_line = parsed_lines[str(i)]
break
# Attach it to the original random line and work out where to go after the line
if original_random_line.size() > 0:
original_random_line["siblings"] += [{ weight = weight, id = str(id) }]
if original_random_line.type != DialogueConstants.TYPE_GOTO:
# Update the next line for all siblings (not goto lines, though, they manager their
# own next ID)
original_random_line["next_id"] = get_line_after_line(id, indent_size, line)
for sibling in original_random_line["siblings"]:
if sibling.id in parsed_lines:
parsed_lines[sibling.id]["next_id"] = original_random_line["next_id"]
line["next_id"] = original_random_line.next_id
# Or set up this line as the original
else:
line["siblings"] = [{ weight = weight, id = str(id) }]
# Find the last weighted random line in this group
for i in range(id, raw_lines.size()):
if i + 1 >= raw_lines.size():
line["next_id"] = DialogueConstants.ID_END
break
if not raw_lines[i + 1].strip_edges().begins_with("%") or get_indent(raw_lines[i + 1]) != indent_size:
line["next_id"] = get_line_after_line(i, indent_size, line)
break
line["next_id"] = get_line_after_line(id, indent_size, line)
if line.next_id == DialogueConstants.ID_NULL:
line["next_id"] = DialogueConstants.ID_END
@ -649,7 +791,6 @@ func find_next_condition_sibling(line_number: int) -> String:
var expected_indent = get_indent(line)
# Look down the list and find an elif or else at the same indent level
var last_valid_id: int = line_number
for i in range(line_number + 1, raw_lines.size()):
line = raw_lines[i]
if is_line_empty(line): continue
@ -670,8 +811,6 @@ func find_next_condition_sibling(line_number: int) -> String:
elif (l.begins_with("elif ") or l.begins_with("else")):
return str(i)
last_valid_id = i
return DialogueConstants.ID_NULL
@ -709,7 +848,7 @@ func find_next_line_after_conditions(line_number: int) -> String:
line_indent = get_indent(line)
if line_indent < expected_indent:
return parsed_lines[str(p)].next_id_after
return parsed_lines[str(p)].get("next_id_after", DialogueConstants.ID_NULL)
return DialogueConstants.ID_END_CONVERSATION
@ -765,7 +904,10 @@ func find_next_line_after_responses(line_number: int) -> String:
elif indent < expected_indent:
# ...outdented so check the previous parent
var previous_parent = parent_stack[parent_stack.size() - 2]
return parsed_lines[str(previous_parent)].next_id_after
if parsed_lines.has(str(previous_parent)):
return parsed_lines[str(previous_parent)].next_id_after
else:
return DialogueConstants.ID_NULL
# We're at the end of a conditional so jump back up to see what's after it
elif line.begins_with("elif ") or line.begins_with("else"):
@ -789,29 +931,52 @@ func find_next_line_after_responses(line_number: int) -> String:
return DialogueConstants.ID_END_CONVERSATION
## Get the names of any autoloads in the project
func get_autoload_names() -> PackedStringArray:
var autoloads: PackedStringArray = []
var project = ConfigFile.new()
project.load("res://project.godot")
if project.has_section("autoload"):
return Array(project.get_section_keys("autoload")).filter(func(key): return key != "DialogueManager")
return autoloads
## Import content from another dialogue file or return an ERR
func import_content(path: String, prefix: String, known_imports: Dictionary) -> Error:
func import_content(path: String, prefix: String, imported_line_map: Dictionary, known_imports: Dictionary) -> Error:
if FileAccess.file_exists(path):
var file = FileAccess.open(path, FileAccess.READ)
var content: PackedStringArray = file.get_as_text().split("\n")
var imported_titles: Dictionary = {}
for line in content:
for index in range(0, content.size()):
var line = content[index]
if is_import_line(line):
var import = extract_import_path_and_name(line)
if import.size() > 0:
if not known_imports.has(import.path.hash()):
# Add an empty record into the keys just so we don't end up with cyclic dependencies
known_imports[import.path.hash()] = ""
if import_content(import.path, import.prefix, known_imports) != OK:
if import_content(import.path, import.prefix, imported_line_map, known_imports) != OK:
return ERR_LINK_FAILED
if not imported_line_map.has(import.path.hash()):
# Make a map so we can refer compiled lines to where they were imported from
imported_line_map[import.path.hash()] = {
hash = import.path.hash(),
imported_on_line_number = index,
from_line = 0,
to_line = 0
}
imported_titles[import.prefix] = import.path.hash()
var origin_hash: int = -1
for hash in known_imports.keys():
if known_imports[hash] == ".":
origin_hash = hash
for hash_value in known_imports.keys():
if known_imports[hash_value] == ".":
origin_hash = hash_value
# Replace any titles or jump points with references to the files they point to (event if they point to their own file)
for i in range(0, content.size()):
@ -851,7 +1016,7 @@ func import_content(path: String, prefix: String, known_imports: Dictionary) ->
content[i] = "%s=> %s/%s" % [line.split("=> ")[0], str(path.hash()), jump]
imported_paths.append(path)
known_imports[path.hash()] = "# %s as %s\n%s\n=> END\n" % [path, path.hash(), "\n".join(content)]
known_imports[path.hash()] = "\n".join(content) + "\n=> END\n"
return OK
else:
return ERR_FILE_NOT_FOUND
@ -897,6 +1062,23 @@ func extract_response_prompt(line: String) -> String:
return line.replace("\\n", "\n").strip_edges()
func parse_response_character_and_text(id: int, text: String, line: Dictionary, indent_size: int, parsed_lines: Dictionary) -> void:
var bits = Array(text.strip_edges().split(": "))
line["character"] = bits.pop_front().strip_edges()
line["character_replacements"] = extract_dialogue_replacements(line.character, line.character.length() + 2 + indent_size)
for replacement in line.character_replacements:
if replacement.has("error"):
add_error(id, replacement.index, replacement.error)
if not line["character"] in character_names:
character_names.append(line["character"])
line["text"] = ": ".join(bits).replace("!ESCAPED_COLON!", ":").strip_edges()
if line.get("translation_key", null) == null:
line["translation_key"] = line.text
func extract_mutation(line: String) -> Dictionary:
var found: RegExMatch = MUTATION_REGEX.search(line)
@ -920,7 +1102,8 @@ func extract_mutation(line: String) -> Dictionary:
}
else:
return {
expression = expression
expression = expression,
is_blocking = not "!" in found.strings[found.names.keyword]
}
else:
@ -1016,13 +1199,41 @@ func extract_goto(line: String) -> String:
return DialogueConstants.ID_ERROR
func extract_markers(line: String) -> Dictionary:
func extract_tags(line: String) -> ResolvedTagData:
var resolved_tags: PackedStringArray = []
var tag_matches: Array[RegExMatch] = TAGS_REGEX.search_all(line)
for tag_match in tag_matches:
line = line.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)
return ResolvedTagData.new({
tags = resolved_tags,
line_without_tags = line
})
func extract_markers(line: String) -> ResolvedLineData:
var text: String = line
var pauses: Dictionary = {}
var speeds: Dictionary = {}
var mutations: Array[Array] = []
var bbcodes: Array = []
var time = null
var time: String = ""
# 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)
@ -1030,7 +1241,7 @@ func extract_markers(line: String) -> Dictionary:
var accumulaive_length_offset = 0
for position in bbcode_positions:
# Ignore our own markers
if position.code in ["wait", "speed", "/speed", "do", "set", "next"]:
if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]:
continue
bbcodes.append({
@ -1055,7 +1266,7 @@ func extract_markers(line: String) -> Dictionary:
var code = bbcode.code
var raw_args = bbcode.raw_args
var args = {}
if code in ["do", "set"]:
if code in ["do", "do!", "set"]:
args["value"] = extract_mutation("%s %s" % [code, raw_args])
else:
# Could be something like:
@ -1078,7 +1289,7 @@ func extract_markers(line: String) -> Dictionary:
speeds[index] = args.get("value").to_float()
"/speed":
speeds[index] = 1.0
"do", "set":
"do", "do!", "set":
mutations.append([index, args.get("value")])
"next":
time = args.get("value") if args.has("value") else "0"
@ -1090,6 +1301,14 @@ func extract_markers(line: String) -> Dictionary:
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)
@ -1097,13 +1316,19 @@ func extract_markers(line: String) -> Dictionary:
for bb in bbcodes:
text = text.insert(bb.start, bb.bbcode)
return {
"text": text,
"pauses": pauses,
"speeds": speeds,
"mutations": mutations,
"time": time
}
# 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)
return ResolvedLineData.new({
text = text,
pauses = pauses,
speeds = speeds,
mutations = mutations,
time = time
})
func find_bbcode_positions_in_string(string: String, find_all: bool = true) -> Array[Dictionary]:
@ -1126,7 +1351,7 @@ func find_bbcode_positions_in_string(string: String, find_all: bool = true) -> A
open_brace_count += 1
else:
if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/"):
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
@ -1136,7 +1361,7 @@ func find_bbcode_positions_in_string(string: String, find_all: bool = true) -> A
if string[i] == "]":
open_brace_count -= 1
if open_brace_count == 0:
if open_brace_count == 0 and not code in ["if", "else", "/if"]:
positions.append({
bbcode = bbcode,
code = code,
@ -1184,7 +1409,7 @@ func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_clo
limit += 1
var token = tokens.pop_front()
var error = check_next_token(token, tokens, line_type)
var error = check_next_token(token, tokens, line_type, expected_close_token)
if error != OK:
return [build_token_tree_error(error, token.index), tokens]
@ -1227,10 +1452,19 @@ func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_clo
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DialogueConstants.TOKEN_ERROR:
return [build_token_tree_error(sub_tree[0][0].value, token.index), 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 == DialogueConstants.TOKEN_VARIABLE and t[i+1].type == DialogueConstants.TOKEN_ASSIGNMENT:
t[i].type = DialogueConstants.TOKEN_STRING
t[i+1].type = DialogueConstants.TOKEN_COLON
t[i+1].erase("value")
tree.append({
type = DialogueConstants.TOKEN_DICTIONARY,
value = tokens_to_dictionary(sub_tree[0])
})
tokens = sub_tree[1]
DialogueConstants.TOKEN_BRACKET_OPEN:
@ -1317,25 +1551,45 @@ func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_clo
})
DialogueConstants.TOKEN_NUMBER:
tree.append({
type = token.type,
value = token.value.to_float() if "." in token.value else token.value.to_int()
})
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 == DialogueConstants.TOKEN_NUMBER:
tree.append(({
type = DialogueConstants.TOKEN_OPERATOR,
value = "-"
}))
tree.append({
type = token.type,
value = -1 * value
})
else:
tree.append({
type = token.type,
value = value
})
if expected_close_token != "":
return [build_token_tree_error(DialogueConstants.ERR_MISSING_CLOSING_BRACKET, tokens[0].index), tokens]
var index: int = tokens[0].index if tokens.size() > 0 else 0
return [build_token_tree_error(DialogueConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens]
return [tree, tokens]
func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String) -> int:
var next_token_type = null
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_type = next_tokens.front().type
next_token = next_tokens.front()
if token.type == DialogueConstants.TOKEN_ASSIGNMENT and line_type == DialogueConstants.TYPE_CONDITION:
# 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 == DialogueConstants.TOKEN_ASSIGNMENT and line_type == DialogueConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token):
return DialogueConstants.ERR_UNEXPECTED_ASSIGNMENT
# Special case for a negative number after this one
if token.type == DialogueConstants.TOKEN_NUMBER and next_token.type == DialogueConstants.TOKEN_NUMBER and next_token.value.begins_with("-"):
return OK
var expected_token_types = []
var unexpected_token_types = []
match token.type:
@ -1364,6 +1618,7 @@ func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_ty
DialogueConstants.TOKEN_BRACE_OPEN:
expected_token_types = [
DialogueConstants.TOKEN_STRING,
DialogueConstants.TOKEN_VARIABLE,
DialogueConstants.TOKEN_NUMBER,
DialogueConstants.TOKEN_BRACE_CLOSE
]
@ -1443,8 +1698,8 @@ func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_ty
DialogueConstants.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:
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 DialogueConstants.ERR_UNEXPECTED_END_OF_EXPRESSION
@ -1503,7 +1758,10 @@ func tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary:
var dictionary = {}
for i in range(0, tokens.size()):
if tokens[i].type == DialogueConstants.TOKEN_COLON:
dictionary[tokens[i-1]] = tokens[i+1]
if tokens.size() == i + 2:
dictionary[tokens[i-1]] = tokens[i+1]
else:
dictionary[tokens[i-1]] = { type = DialogueConstants.TOKEN_GROUP, value = tokens.slice(i+1) }
return dictionary

View File

@ -0,0 +1,15 @@
extends RefCounted
var text: String = ""
var pauses: Dictionary = {}
var speeds: Dictionary = {}
var mutations: Array[Array] = []
var time: String = ""
func _init(data: Dictionary) -> void:
text = data.text
pauses = data.pauses
speeds = data.speeds
mutations = data.mutations
time = data.time

View File

@ -0,0 +1,10 @@
extends RefCounted
var tags: PackedStringArray = []
var line_without_tags: String = ""
func _init(data: Dictionary) -> void:
tags = data.tags
line_without_tags = data.line_without_tags

View File

@ -6,7 +6,7 @@ signal open_requested()
signal close_requested()
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("../constants.gd")
@onready var input: LineEdit = $Search/Input
@ -14,6 +14,7 @@ const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
@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
@ -40,32 +41,38 @@ var result_index: int = -1:
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() })
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()
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")
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")
@ -76,26 +83,26 @@ func apply_theme() -> void:
# 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)
@ -111,8 +118,13 @@ func find_in_line(line: String, text: String, from_index: int = 0) -> int:
func _on_text_edit_gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed() and event.as_text() == "Ctrl+F":
emit_signal("open_requested")
if event is InputEventKey and event.is_pressed():
match event.as_text():
"Ctrl+F", "Command+F":
open_requested.emit()
"Ctrl+Shift+R", "Command+Shift+R":
replace_check_button.set_pressed(true)
open_requested.emit()
func _on_text_edit_text_changed() -> void:
@ -160,7 +172,7 @@ func _on_input_gui_input(event: InputEvent) -> void:
func _on_replace_button_pressed() -> void:
if result_index == -1: return
# Replace the selection at result index
var r: Array = results[result_index]
var lines: PackedStringArray = code_edit.text.split("\n")
@ -169,6 +181,7 @@ func _on_replace_button_pressed() -> void:
lines[r[0]] = line
code_edit.text = "\n".join(lines)
search(input.text, result_index)
code_edit.text_changed.emit()
func _on_replace_all_button_pressed() -> void:
@ -177,6 +190,7 @@ func _on_replace_all_button_pressed() -> void:
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:

View File

@ -1,20 +1,9 @@
[gd_scene load_steps=4 format=3 uid="uid://gr8nakpbrhby"]
[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"]
[sub_resource type="Image" id="Image_mirhx"]
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_wy68i"]
image = SubResource("Image_mirhx")
[node name="SearchAndReplace" type="VBoxContainer"]
visible = false
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 31.0
@ -28,6 +17,7 @@ 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"]
@ -39,7 +29,7 @@ layout_mode = 2
[node name="PreviousButton" type="Button" parent="Search"]
layout_mode = 2
icon = SubResource("ImageTexture_wy68i")
tooltip_text = "Previous"
flat = true
[node name="ResultLabel" type="Label" parent="Search"]
@ -48,7 +38,7 @@ text = "0 of 0"
[node name="NextButton" type="Button" parent="Search"]
layout_mode = 2
icon = SubResource("ImageTexture_wy68i")
tooltip_text = "Next"
flat = true
[node name="VSeparator2" type="VSeparator" parent="Search"]
@ -79,7 +69,7 @@ flat = true
[node name="ReplaceAllButton" type="Button" parent="Replace"]
layout_mode = 2
disabled = true
text = "Replace All"
text = "Replace all"
flat = true
[connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"]

View File

@ -4,7 +4,7 @@ extends VBoxContainer
signal title_selected(title: String)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("../constants.gd")
@onready var filter_edit: LineEdit = $FilterEdit
@ -27,8 +27,8 @@ var filter: String:
func _ready() -> void:
apply_theme()
filter_edit.placeholder_text = DialogueConstants.translate("titles_list.filter")
filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter")
func select_title(title: String) -> void:

View File

@ -1,10 +1,10 @@
@tool
extends Button
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases"
const LOCAL_CONFIG_PATH = "res://addons/dialogue_manager/plugin.cfg"
@onready var http_request: HTTPRequest = $HTTPRequest
@ -34,13 +34,6 @@ func _ready() -> void:
timer.start(60 * 60 * 12)
# Get the current version
func get_version() -> String:
var config: ConfigFile = ConfigFile.new()
config.load(LOCAL_CONFIG_PATH)
return config.get_value("plugin", "version")
# Convert a version number to an actually comparable number
func version_to_number(version: String) -> int:
var bits = version.split(".")
@ -63,7 +56,8 @@ func apply_theme() -> void:
func check_for_update() -> void:
http_request.request(REMOTE_RELEASES_URL)
if DialogueSettings.get_user_value("check_for_updates", true):
http_request.request(REMOTE_RELEASES_URL)
### Signals
@ -72,7 +66,7 @@ func check_for_update() -> void:
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 = get_version()
var current_version: String = editor_plugin.get_version()
# Work out the next version from the releases information on GitHub
var response = JSON.parse_string(body.get_string_from_utf8())
@ -85,7 +79,7 @@ func _on_http_request_request_completed(result: int, response_code: int, headers
)
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) })
text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) })
show()
@ -107,19 +101,19 @@ func _on_download_dialog_close_requested() -> void:
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.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")
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.dialog_text = DialogueConstants.translate(&"update.failed")
update_failed_dialog.popup_centered()

View File

@ -6,56 +6,56 @@ const CACHE_PATH = "user://dialogue_manager_cache.json"
# 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_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_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_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_ERROR = "error"
const TOKEN_ERROR = &"error"
# Line types
const TYPE_UNKNOWN = "unknown"
const TYPE_RESPONSE = "response"
const TYPE_TITLE = "title"
const TYPE_CONDITION = "condition"
const TYPE_MUTATION = "mutation"
const TYPE_GOTO = "goto"
const TYPE_DIALOGUE = "dialogue"
const TYPE_ERROR = "error"
const TYPE_UNKNOWN = &"unknown"
const TYPE_RESPONSE = &"response"
const TYPE_TITLE = &"title"
const TYPE_CONDITION = &"condition"
const TYPE_MUTATION = &"mutation"
const TYPE_GOTO = &"goto"
const TYPE_DIALOGUE = &"dialogue"
const TYPE_ERROR = &"error"
const TYPE_ELSE = "else"
const TYPE_ELSE = &"else"
# 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!"
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
@ -94,88 +94,94 @@ 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
## Get the error message
static func get_error_message(error: int) -> String:
match error:
ERR_ERRORS_IN_IMPORTED_FILE:
return translate("errors.import_errors")
return translate(&"errors.import_errors")
ERR_FILE_ALREADY_IMPORTED:
return translate("errors.already_imported")
return translate(&"errors.already_imported")
ERR_DUPLICATE_IMPORT_NAME:
return translate("errors.duplicate_import")
return translate(&"errors.duplicate_import")
ERR_EMPTY_TITLE:
return translate("errors.empty_title")
return translate(&"errors.empty_title")
ERR_DUPLICATE_TITLE:
return translate("errors.duplicate_title")
return translate(&"errors.duplicate_title")
ERR_NESTED_TITLE:
return translate("errors.nested_title")
return translate(&"errors.nested_title")
ERR_TITLE_INVALID_CHARACTERS:
return translate("errors.invalid_title_string")
return translate(&"errors.invalid_title_string")
ERR_TITLE_BEGINS_WITH_NUMBER:
return translate("errors.invalid_title_number")
return translate(&"errors.invalid_title_number")
ERR_UNKNOWN_TITLE:
return translate("errors.unknown_title")
return translate(&"errors.unknown_title")
ERR_INVALID_TITLE_REFERENCE:
return translate("errors.jump_to_invalid_title")
return translate(&"errors.jump_to_invalid_title")
ERR_TITLE_REFERENCE_HAS_NO_CONTENT:
return translate("errors.title_has_no_content")
return translate(&"errors.title_has_no_content")
ERR_INVALID_EXPRESSION:
return translate("errors.invalid_expression")
return translate(&"errors.invalid_expression")
ERR_UNEXPECTED_CONDITION:
return translate("errors.unexpected_condition")
return translate(&"errors.unexpected_condition")
ERR_DUPLICATE_ID:
return translate("errors.duplicate_id")
return translate(&"errors.duplicate_id")
ERR_MISSING_ID:
return translate("errors.missing_id")
return translate(&"errors.missing_id")
ERR_INVALID_INDENTATION:
return translate("errors.invalid_indentation")
return translate(&"errors.invalid_indentation")
ERR_INVALID_CONDITION_INDENTATION:
return translate("errors.condition_has_no_content")
return translate(&"errors.condition_has_no_content")
ERR_INCOMPLETE_EXPRESSION:
return translate("errors.incomplete_expression")
return translate(&"errors.incomplete_expression")
ERR_INVALID_EXPRESSION_FOR_VALUE:
return translate("errors.invalid_expression_for_value")
return translate(&"errors.invalid_expression_for_value")
ERR_FILE_NOT_FOUND:
return translate("errors.file_not_found")
return translate(&"errors.file_not_found")
ERR_UNEXPECTED_END_OF_EXPRESSION:
return translate("errors.unexpected_end_of_expression")
return translate(&"errors.unexpected_end_of_expression")
ERR_UNEXPECTED_FUNCTION:
return translate("errors.unexpected_function")
return translate(&"errors.unexpected_function")
ERR_UNEXPECTED_BRACKET:
return translate("errors.unexpected_bracket")
return translate(&"errors.unexpected_bracket")
ERR_UNEXPECTED_CLOSING_BRACKET:
return translate("errors.unexpected_closing_bracket")
return translate(&"errors.unexpected_closing_bracket")
ERR_MISSING_CLOSING_BRACKET:
return translate("errors.missing_closing_bracket")
return translate(&"errors.missing_closing_bracket")
ERR_UNEXPECTED_OPERATOR:
return translate("errors.unexpected_operator")
return translate(&"errors.unexpected_operator")
ERR_UNEXPECTED_COMMA:
return translate("errors.unexpected_comma")
return translate(&"errors.unexpected_comma")
ERR_UNEXPECTED_COLON:
return translate("errors.unexpected_colon")
return translate(&"errors.unexpected_colon")
ERR_UNEXPECTED_DOT:
return translate("errors.unexpected_dot")
return translate(&"errors.unexpected_dot")
ERR_UNEXPECTED_BOOLEAN:
return translate("errors.unexpected_boolean")
return translate(&"errors.unexpected_boolean")
ERR_UNEXPECTED_STRING:
return translate("errors.unexpected_string")
return translate(&"errors.unexpected_string")
ERR_UNEXPECTED_NUMBER:
return translate("errors.unexpected_number")
return translate(&"errors.unexpected_number")
ERR_UNEXPECTED_VARIABLE:
return translate("errors.unexpected_variable")
return translate(&"errors.unexpected_variable")
ERR_INVALID_INDEX:
return translate("errors.invalid_index")
return translate(&"errors.invalid_index")
ERR_UNEXPECTED_ASSIGNMENT:
return translate("errors.unexpected_assignment")
return translate(&"errors.unexpected_assignment")
ERR_UNKNOWN_USING:
return translate(&"errors.unknown_using")
return translate("errors.unknown")
return translate(&"errors.unknown")
static func translate(string: String) -> String:
var language: String = TranslationServer.get_tool_locale().substr(0, 2)
var translations_path: String = "res://addons/dialogue_manager/l10n/%s.po" % language
var fallback_translations_path: String = "res://addons/dialogue_manager/l10n/en.po"
var translations: Translation = load(translations_path if FileAccess.file_exists(translations_path) else fallback_translations_path)
var base_path = new().get_script().resource_path.get_base_dir()
var language: String = TranslationServer.get_tool_locale()
var translations_path: String = "%s/l10n/%s.po" % [base_path, language]
var fallback_translations_path: String = "%s/l10n/%s.po" % [base_path, TranslationServer.get_tool_locale().substr(0, 2)]
var en_translations_path: String = "%s/l10n/en.po" % base_path
var translations: Translation = load(translations_path if FileAccess.file_exists(translations_path) else (fallback_translations_path if FileAccess.file_exists(fallback_translations_path) else en_translations_path))
return translations.get_message(string)

View File

@ -1,21 +1,46 @@
extends RichTextLabel
@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: String = "ui_cancel"
# The action to press to skip typing.
@export var skip_action: StringName = &"ui_cancel"
## The speed with which the text types out
## 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
## Automatically have a brief pause when these characters are encountered.
@export var pause_at_characters: String = ".?!"
## Don't auto pause if the charcter 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
## The current line of dialogue.
var dialogue_line:
set(next_dialogue_line):
dialogue_line = next_dialogue_line
@ -24,102 +49,119 @@ var dialogue_line:
get:
return dialogue_line
var last_wait_index: int = -1
var last_mutation_index: int = -1
var waiting_seconds: float = 0
## Whether the label is currently typing itself out.
var is_typing: bool = false:
set(value):
if is_typing != value and value == false:
finished_typing.emit()
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 _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)
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:
if self.is_typing and visible_ratio < 1 and event.is_action_pressed(skip_action):
# Run any inline mutations that haven't been run yet
for i in range(visible_characters, get_total_character_count()):
mutate_inline_mutations(i)
visible_characters = get_total_character_count()
self.is_typing = false
finished_typing.emit()
# 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
## Start typing out the text
func type_out() -> void:
text = dialogue_line.text
visible_characters = 0
self.is_typing = true
waiting_seconds = 0
visible_ratio = 0
_waiting_seconds = 0
_last_wait_index = -1
_last_mutation_index = -1
# Text isn't calculated until the next frame
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:
# Run any inline mutations
for i in range(0, get_total_character_count()):
mutate_inline_mutations(i)
_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:
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 _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)
var additional_waiting_seconds: float = _get_pause(visible_characters)
# Pause on characters like "."
if visible_characters > 0 and get_parsed_text()[visible_characters - 1] in pause_at_characters.split():
additional_waiting_seconds += seconds_per_step * 15
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))
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))
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))
seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters))
if seconds_needed > delta:
waiting_seconds += seconds_needed
_waiting_seconds += seconds_needed
else:
type_next(delta, seconds_needed)
_type_next(delta, seconds_needed)
# Get the pause for the current typing position if there is one
func get_pause(at_index: int) -> float:
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:
func _get_speed(at_index: int) -> float:
var speed: float = 1
for index in dialogue_line.speeds:
if index > at_index:
@ -128,12 +170,57 @@ func get_speed(at_index: int) -> float:
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:
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:
_is_awaiting_mutation = true
# The DialogueManager can't be referenced directly here so we need to get it by its path
Engine.get_singleton("DialogueManager").mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
await Engine.get_singleton("DialogueManager").mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
_is_awaiting_mutation = false
# 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)) == 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()

View File

@ -16,3 +16,4 @@ 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")

View File

@ -1,43 +1,98 @@
## A line of dialogue returned from [code]DialogueManager[/code].
class_name DialogueLine extends RefCounted
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueResponse = preload("res://addons/dialogue_manager/dialogue_response.gd")
const _DialogueConstants = preload("./constants.gd")
var type: String = DialogueConstants.TYPE_DIALOGUE
## 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 = _DialogueConstants.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[DialogueResponse] = []
## A list of any extra game states to check when resolving variables and mutations.
var extra_game_states: Array = []
var time = null
## 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.extra_game_states
extra_game_states = data.get("extra_game_states", [])
match type:
DialogueConstants.TYPE_DIALOGUE:
_DialogueConstants.TYPE_DIALOGUE:
character = data.character
character_replacements = data.character_replacements
character_replacements = data.get("character_replacements", [] as Array[Dictionary])
text = data.text
text_replacements = data.text_replacements
translation_key = data.translation_key
pauses = data.pauses
speeds = data.speeds
inline_mutations = data.inline_mutations
time = data.time
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", [])
DialogueConstants.TYPE_MUTATION:
_DialogueConstants.TYPE_MUTATION:
mutation = data.mutation
func _to_string() -> String:
match type:
_DialogueConstants.TYPE_DIALOGUE:
return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text]
_DialogueConstants.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 ""

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,131 @@
@icon("./assets/responses_menu.svg")
## A VBoxContainer for dialogue responses provided by [b]Dialogue Manager[/b].
class_name DialogueResponsesMenu extends VBoxContainer
## 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 = &""
# The list of dialogue responses.
var responses: Array = []:
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:
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 = String(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:
get_menu_items()[0].grab_focus()
)
if is_instance_valid(response_template):
response_template.hide()
# This is deprecated.
func set_responses(next_responses: Array) -> void:
self.responses = next_responses
# 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_previous = item.get_path()
else:
item.focus_neighbor_top = 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_next = item.get_path()
else:
item.focus_neighbor_bottom = 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()
## 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
### 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
get_viewport().set_input_as_handled()
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
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():
response_selected.emit(response)

View File

@ -1,20 +1,42 @@
@tool
@icon("./assets/icon.svg")
## A collection of dialogue lines for use with [code]DialogueManager[/code].
class_name DialogueResource extends Resource
const DialogueManager = preload("res://addons/dialogue_manager/dialogue_manager.gd")
const _DialogueManager = preload("./dialogue_manager.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
func get_next_dialogue_line(title: String, extra_game_states: Array = [], mutation_behaviour: DialogueManager.MutationBehaviour = DialogueManager.MutationBehaviour.Wait) -> DialogueLine:
## 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: _DialogueManager.MutationBehaviour = _DialogueManager.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())]

View File

@ -1,22 +1,62 @@
## A response to a line of dialogue, usualy attached to a [code]DialogueLine[/code].
class_name DialogueResponse extends RefCounted
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const _DialogueConstants = preload("./constants.gd")
var type: String = DialogueConstants.TYPE_RESPONSE
## 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 = _DialogueConstants.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
## 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
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 ""

View File

@ -1,7 +1,9 @@
extends EditorTranslationParserPlugin
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("./constants.gd")
const DialogueSettings = preload("./settings.gd")
const DialogueManagerParseResult = preload("./components/parse_result.gd")
func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> void:
@ -11,14 +13,15 @@ func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> v
var data: DialogueManagerParseResult = DialogueManagerParser.parse_string(text, path)
var known_keys: PackedStringArray = PackedStringArray([])
# Add all character names
var character_names: PackedStringArray = data.character_names
for character_name in character_names:
if character_name in known_keys: continue
# Add all character names if settings ask for it
if DialogueSettings.get_setting("export_characters_in_translation", true):
var character_names: PackedStringArray = data.character_names
for character_name in character_names:
if character_name in known_keys: continue
known_keys.append(character_name)
known_keys.append(character_name)
msgids_context_plural.append([character_name, "dialogue", ""])
msgids_context_plural.append([character_name.replace('"', '\\"'), "dialogue", ""])
# Add all dialogue lines and responses
var dialogue: Dictionary = data.lines
@ -31,9 +34,9 @@ func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> v
known_keys.append(line.translation_key)
if line.translation_key == "" or line.translation_key == line.text:
msgids_context_plural.append([line.text, "", ""])
msgids_context_plural.append([line.text.replace('"', '\\"'), "", ""])
else:
msgids_context_plural.append([line.text, line.translation_key, ""])
msgids_context_plural.append([line.text.replace('"', '\\"'), line.translation_key.replace('"', '\\"'), ""])
func _get_recognized_extensions() -> PackedStringArray:

View File

@ -0,0 +1,204 @@
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
{
isWaitingForInput = false;
balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
if (value == null)
{
QueueFree();
return;
}
dialogueLine = value;
UpdateDialogue();
}
}
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);
}));
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 async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null)
{
temporaryGameStates = 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 UpdateDialogue()
{
if (!IsNodeReady())
{
await ToSignal(this, SignalName.Ready);
}
// 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;
GetTree().CreateTimer(0.1f).Timeout += () =>
{
if (willHideBalloon)
{
willHideBalloon = false;
balloon.Hide();
}
};
}
#endregion
}
}

View File

@ -1,12 +1,15 @@
extends CanvasLayer
## The action to use for advancing the dialogue
@export var next_action: StringName = &"ui_accept"
@onready var balloon: ColorRect = $Balloon
@onready var margin: MarginContainer = $Balloon/Margin
@onready var character_label: RichTextLabel = $Balloon/Margin/VBox/CharacterLabel
@onready var dialogue_label := $Balloon/Margin/VBox/DialogueLabel
@onready var responses_menu: VBoxContainer = $Balloon/Margin/VBox/Responses
@onready var response_template: RichTextLabel = %ResponseTemplate
## The action to use to skip typing the dialogue
@export var skip_action: StringName = &"ui_cancel"
@onready var balloon: Control = %Balloon
@onready var character_label: RichTextLabel = %CharacterLabel
@onready var dialogue_label: DialogueLabel = %DialogueLabel
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
## The dialogue resource
var resource: DialogueResource
@ -22,191 +25,117 @@ var will_hide_balloon: bool = false
## The current line
var dialogue_line: DialogueLine:
set(next_dialogue_line):
is_waiting_for_input = false
set(next_dialogue_line):
is_waiting_for_input = false
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
if not next_dialogue_line:
queue_free()
return
# The dialogue has finished so close the balloon
if not next_dialogue_line:
queue_free()
return
# Remove any previous responses
for child in responses_menu.get_children():
responses_menu.remove_child(child)
child.queue_free()
# If the node isn't ready yet then none of the labels will be ready yet either
if not is_node_ready():
await ready
dialogue_line = next_dialogue_line
dialogue_line = next_dialogue_line
character_label.visible = not dialogue_line.character.is_empty()
character_label.text = tr(dialogue_line.character, "dialogue")
character_label.visible = not dialogue_line.character.is_empty()
character_label.text = tr(dialogue_line.character, "dialogue")
dialogue_label.modulate.a = 0
dialogue_label.custom_minimum_size.x = dialogue_label.get_parent().size.x - 1
dialogue_label.dialogue_line = dialogue_line
dialogue_label.hide()
dialogue_label.dialogue_line = dialogue_line
# Show any responses we have
responses_menu.modulate.a = 0
if dialogue_line.responses.size() > 0:
for response in dialogue_line.responses:
# Duplicate the template so we can grab the fonts, sizing, etc
var item: RichTextLabel = response_template.duplicate(0)
item.name = "Response%d" % responses_menu.get_child_count()
if not response.is_allowed:
item.name = String(item.name) + "Disallowed"
item.modulate.a = 0.4
item.text = response.text
item.show()
responses_menu.add_child(item)
responses_menu.hide()
responses_menu.set_responses(dialogue_line.responses)
# Show our balloon
balloon.show()
will_hide_balloon = false
# Show our balloon
balloon.show()
will_hide_balloon = false
dialogue_label.modulate.a = 1
if not dialogue_line.text.is_empty():
dialogue_label.type_out()
await dialogue_label.finished_typing
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:
responses_menu.modulate.a = 1
configure_menu()
elif dialogue_line.time != null:
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()
get:
return dialogue_line
# 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()
get:
return dialogue_line
func _ready() -> void:
response_template.hide()
balloon.hide()
balloon.custom_minimum_size.x = balloon.get_viewport_rect().size.x
balloon.hide()
Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated)
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
func _unhandled_input(_event: InputEvent) -> void:
# Only the balloon is allowed to handle input while it's showing
get_viewport().set_input_as_handled()
# Only the balloon is allowed to handle input while it's showing
get_viewport().set_input_as_handled()
## Start some dialogue
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
temporary_game_states = extra_game_states
is_waiting_for_input = false
resource = dialogue_resource
self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states)
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)
## 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)
### Helpers
# Set up keyboard movement and signals for the response menu
func configure_menu() -> void:
balloon.focus_mode = Control.FOCUS_NONE
var items = get_responses()
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_previous = item.get_path()
else:
item.focus_neighbor_top = 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_next = item.get_path()
else:
item.focus_neighbor_bottom = 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))
items[0].grab_focus()
# Get a list of enabled items
func get_responses() -> Array:
var items: Array = []
for child in responses_menu.get_children():
if "Disallowed" in child.name: continue
items.append(child)
return items
func handle_resize() -> void:
if not is_instance_valid(margin):
call_deferred("handle_resize")
return
balloon.custom_minimum_size.y = margin.size.y
# Force a resize on only the height
balloon.size.y = 0
var viewport_size = balloon.get_viewport_rect().size
balloon.global_position = Vector2((viewport_size.x - balloon.size.x) * 0.5, viewport_size.y - balloon.size.y)
self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states)
### Signals
func _on_mutated(_mutation: Dictionary) -> void:
is_waiting_for_input = false
will_hide_balloon = true
get_tree().create_timer(0.1).timeout.connect(func():
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
)
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) -> void:
if "Disallowed" in item.name: return
if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1:
next(dialogue_line.responses[item.get_index()].next_id)
elif event.is_action_pressed("ui_accept") and item in get_responses():
next(dialogue_line.responses[item.get_index()].next_id)
is_waiting_for_input = false
will_hide_balloon = true
get_tree().create_timer(0.1).timeout.connect(func():
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
)
func _on_balloon_gui_input(event: InputEvent) -> void:
if not is_waiting_for_input: return
if dialogue_line.responses.size() > 0: return
# 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
# When there are no response options the balloon itself is the clickable thing
get_viewport().set_input_as_handled()
if not is_waiting_for_input: return
if dialogue_line.responses.size() > 0: return
if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1:
next(dialogue_line.next_id)
elif event.is_action_pressed("ui_accept") and get_viewport().gui_get_focus_owner() == balloon:
next(dialogue_line.next_id)
# 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_margin_resized() -> void:
handle_resize()
func _on_responses_menu_response_selected(response: DialogueResponse) -> void:
next(response.next_id)

View File

@ -1,45 +1,110 @@
[gd_scene load_steps=5 format=3 uid="uid://73jm5qjy52vq"]
[gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"]
[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_4u26j"]
[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" 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" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_72ixx"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5d24i"]
content_margin_left = 40.0
content_margin_top = 5.0
content_margin_right = 5.0
content_margin_bottom = 5.0
bg_color = Color(1, 1, 1, 0.25098)
[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_oj3c8"]
content_margin_left = 40.0
content_margin_top = 5.0
content_margin_right = 5.0
content_margin_bottom = 5.0
draw_center = false
[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_uy0d5"]
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
Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5")
[node name="ExampleBalloon" type="CanvasLayer"]
layer = 100
script = ExtResource("1_4u26j")
script = ExtResource("1_36de5")
[node name="Balloon" type="ColorRect" parent="."]
color = Color(0, 0, 0, 1)
[node name="Margin" type="MarginContainer" parent="Balloon"]
layout_mode = 0
[node name="Balloon" type="Control" parent="."]
unique_name_in_owner = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
offset_bottom = 119.0
anchor_bottom = 1.0
grow_horizontal = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 10
metadata/_edit_layout_mode = 1
grow_vertical = 2
theme = SubResource("Theme_qq3yp")
[node name="VBox" type="VBoxContainer" parent="Balloon/Margin"]
[node name="Panel" type="Panel" parent="Balloon"]
clip_children = 2
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 21.0
offset_top = -183.0
offset_right = -19.0
offset_bottom = -19.0
grow_horizontal = 2
grow_vertical = 0
mouse_filter = 1
[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Margin/VBox"]
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.501961)
layout_mode = 2
mouse_filter = 1
@ -48,26 +113,37 @@ text = "Character"
fit_content = true
scroll_active = false
[node name="DialogueLabel" parent="Balloon/Margin/VBox" instance=ExtResource("2_a8ve6")]
layout_mode = 2
text = "Dialogue"
[node name="Responses" type="VBoxContainer" parent="Balloon/Margin/VBox"]
layout_mode = 2
theme_override_constants/separation = 2
[node name="ResponseTemplate" type="RichTextLabel" parent="Balloon/Margin/VBox"]
[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_a8ve6")]
unique_name_in_owner = true
layout_mode = 2
theme_override_styles/focus = SubResource("StyleBoxFlat_5d24i")
theme_override_styles/normal = SubResource("StyleBoxFlat_oj3c8")
bbcode_enabled = true
text = "Response"
fit_content = true
scroll_active = false
shortcut_keys_enabled = false
meta_underlined = false
hint_underlined = false
size_flags_vertical = 3
text = "Dialogue..."
[node name="Responses" type="MarginContainer" parent="Balloon"]
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -147.0
offset_top = -558.0
offset_right = 494.0
offset_bottom = -154.0
grow_horizontal = 2
grow_vertical = 0
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses" node_paths=PackedStringArray("response_template")]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 8
theme_override_constants/separation = 2
script = ExtResource("3_72ixx")
response_template = NodePath("ResponseExample")
[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"]
layout_mode = 2
text = "Response example"
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
[connection signal="resized" from="Balloon/Margin" to="." method="_on_margin_resized"]
[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]

View File

@ -1,87 +1,173 @@
[gd_scene load_steps=8 format=3 uid="uid://b361p61jmf257"]
[gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"]
[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_4u26j"]
[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"]
[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" 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" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_1j1j0"]
[sub_resource type="Theme" id="Theme_isg48"]
default_font_size = 9
[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="Theme" id="Theme_owda0"]
default_font_size = 9
[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="Theme" id="Theme_fakos"]
default_font_size = 9
[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_5d24i"]
content_margin_left = 20.0
content_margin_top = 2.0
content_margin_right = 2.0
content_margin_bottom = 2.0
bg_color = Color(1, 1, 1, 0.25098)
[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_oj3c8"]
content_margin_left = 20.0
content_margin_top = 2.0
content_margin_right = 2.0
content_margin_bottom = 2.0
draw_center = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"]
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
Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5")
[node name="ExampleBalloon" type="CanvasLayer"]
layer = 100
script = ExtResource("1_4u26j")
script = ExtResource("1_s2gbs")
[node name="Balloon" type="ColorRect" parent="."]
color = Color(0, 0, 0, 1)
[node name="Margin" type="MarginContainer" parent="Balloon"]
layout_mode = 1
anchors_preset = 10
[node name="Balloon" type="Control" parent="."]
unique_name_in_owner = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
offset_bottom = 75.0
anchor_bottom = 1.0
grow_horizontal = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 10
metadata/_edit_layout_mode = 1
grow_vertical = 2
theme = SubResource("Theme_qq3yp")
[node name="VBox" type="VBoxContainer" parent="Balloon/Margin"]
[node name="Panel" type="Panel" parent="Balloon"]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 3.0
offset_top = -62.0
offset_right = -4.0
offset_bottom = -4.0
grow_horizontal = 2
grow_vertical = 0
[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Margin/VBox"]
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.501961)
layout_mode = 2
mouse_filter = 1
theme = SubResource("Theme_isg48")
bbcode_enabled = true
text = "Character"
fit_content = true
scroll_active = false
[node name="DialogueLabel" parent="Balloon/Margin/VBox" instance=ExtResource("2_a8ve6")]
layout_mode = 2
theme = SubResource("Theme_owda0")
text = "Dialogue"
[node name="Responses" type="VBoxContainer" parent="Balloon/Margin/VBox"]
layout_mode = 2
theme_override_constants/separation = 1
[node name="ResponseTemplate" type="RichTextLabel" parent="Balloon/Margin/VBox"]
[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_hfvdi")]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 2
theme = SubResource("Theme_fakos")
theme_override_styles/focus = SubResource("StyleBoxFlat_5d24i")
theme_override_styles/normal = SubResource("StyleBoxFlat_oj3c8")
bbcode_enabled = true
text = "Response"
fit_content = true
scroll_active = false
shortcut_keys_enabled = false
meta_underlined = false
hint_underlined = false
size_flags_vertical = 3
text = "Dialogue..."
skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex")
[node name="Responses" type="MarginContainer" parent="Balloon"]
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -124.0
offset_top = -218.0
offset_right = 125.0
offset_bottom = -50.0
grow_horizontal = 2
grow_vertical = 0
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 8
theme_override_constants/separation = 2
script = ExtResource("3_1j1j0")
[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"]
layout_mode = 2
text = "Response Example"
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
[connection signal="resized" from="Balloon/Margin" to="." method="_on_margin_resized"]
[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]

View File

@ -5,11 +5,10 @@ extends EditorImportPlugin
signal compiled_resource(resource: Resource)
const DialogueResource = preload("res://addons/dialogue_manager/dialogue_resource.gd")
const compiler_version = 8
const DialogueResource = preload("./dialogue_resource.gd")
const DialogueManagerParseResult = preload("./components/parse_result.gd")
var editor_plugin
const compiler_version = 11
func _get_importer_name() -> String:
@ -63,26 +62,24 @@ func _get_option_visibility(path: String, option_name: StringName, options: Dict
func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error:
return compile_file(source_file, "%s.%s" % [save_path, _get_save_extension()])
var cache = Engine.get_meta("DialogueCache")
func compile_file(path: String, resource_path: String, will_cascade_cache_data: bool = true) -> Error:
# Get the raw file contents
if not FileAccess.file_exists(path): return ERR_FILE_NOT_FOUND
if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var file: FileAccess = FileAccess.open(source_file, FileAccess.READ)
var raw_text: String = file.get_as_text()
# Parse the text
var parser: DialogueManagerParser = DialogueManagerParser.new()
var err: Error = parser.parse(raw_text, path)
var err: Error = parser.parse(raw_text, source_file)
var data: DialogueManagerParseResult = parser.get_data()
var errors: Array[Dictionary] = parser.get_errors()
parser.free()
if err != OK:
printerr("%d errors found in %s" % [errors.size(), path])
editor_plugin.add_errors_to_dialogue_file_cache(path, errors)
printerr("%d errors found in %s" % [errors.size(), source_file])
cache.add_errors_to_file(source_file, errors)
return err
# Get the current addon version
@ -94,16 +91,23 @@ func compile_file(path: String, resource_path: String, will_cascade_cache_data:
var resource: DialogueResource = DialogueResource.new()
resource.set_meta("dialogue_manager_version", version)
resource.using_states = data.using_states
resource.titles = data.titles
resource.first_title = data.first_title
resource.character_names = data.character_names
resource.lines = data.lines
resource.raw_text = data.raw_text
if will_cascade_cache_data:
editor_plugin.add_to_dialogue_file_cache(path, resource_path, data)
# Clear errors and possibly trigger any cascade recompiles
cache.add_file(source_file, data)
err = ResourceSaver.save(resource, resource_path)
err = 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

Binary file not shown.

View File

@ -30,6 +30,9 @@ msgstr "Clear recent files"
msgid "save_all_files"
msgstr "Save all files"
msgid "find_in_files"
msgstr "Find in files..."
msgid "test_dialogue"
msgstr "Test dialogue"
@ -45,6 +48,12 @@ msgstr "Translations"
msgid "settings"
msgstr "Settings"
msgid "sponsor"
msgstr "Sponsor"
msgid "show_support"
msgstr "Support Dialogue Manager"
msgid "docs"
msgstr "Docs"
@ -132,11 +141,23 @@ msgstr "Copy file path"
msgid "buffer.show_in_filesystem"
msgstr "Show in FileSystem"
msgid "settings.invalid_test_scene"
msgstr "\"{path}\" does not extend BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Revert to default test scene"
msgid "settings.default_balloon_hint"
msgstr "Custom balloon to use when calling \"DialogueManager.show_balloon()\""
msgid "settings.revert_to_default_balloon"
msgstr "Revert to default balloon"
msgid "settings.default_balloon_path"
msgstr "<example balloon>"
msgid "settings.autoload"
msgstr "Autload"
msgstr "Autoload"
msgid "settings.path"
msgstr "Path"
@ -150,15 +171,24 @@ msgstr "Treat missing translation keys as errors"
msgid "settings.missing_keys_hint"
msgstr "If you are using static translation keys then having this enabled will help you find any lines that you haven't added a key to yet."
msgid "settings.characters_translations"
msgstr "Export character names in translation files"
msgid "settings.wrap_long_lines"
msgstr "Wrap long lines"
msgid "settings.include_failed_responses"
msgstr "Include responses with failed conditions"
msgid "settings.ignore_missing_state_values"
msgstr "Skip over missing state value errors (not recommended)"
msgid "settings.custom_test_scene"
msgstr "Custom test scene (must extend BaseDialogueTestScene)"
msgid "settings.default_csv_locale"
msgstr "Default CSV Locale"
msgid "settings.states_shortcuts"
msgstr "State Shortcuts"
@ -168,9 +198,45 @@ msgstr "If an autoload is enabled here you can refer to its properties and metho
msgid "settings.states_hint"
msgstr "ie. Instead of \"SomeState.some_property\" you could just use \"some_property\""
msgid "settings.recompile_warning"
msgstr "Changing these settings will force a recompile of all dialogue. Only change them if you know what you are doing."
msgid "settings.create_lines_for_responses_with_characters"
msgstr "Create child dialogue line for responses with character names in them"
msgid "settings.open_in_external_editor"
msgstr "Open dialogue files in external editor"
msgid "settings.external_editor_warning"
msgstr "Note: Syntax highlighting and detailed error checking are not supported in external editors."
msgid "settings.include_characters_in_translations"
msgstr "Include character names in translation exports"
msgid "settings.include_notes_in_translations"
msgstr "Include notes (## comments) in translation exports"
msgid "settings.check_for_updates"
msgstr "Check for updates"
msgid "n_of_n"
msgstr "{index} of {total}"
msgid "search.find"
msgstr "Find:"
msgid "search.find_all"
msgstr "Find all..."
msgid "search.placeholder"
msgstr "Text to search for"
msgid "search.replace_placeholder"
msgstr "Text to replace it with"
msgid "search.replace_selected"
msgstr "Replace selected"
msgid "search.previous"
msgstr "Previous"
@ -198,6 +264,9 @@ msgstr "Filter files"
msgid "titles_list.filter"
msgstr "Filter titles"
msgid "errors.key_not_found"
msgstr "Key \"{key}\" not found."
msgid "errors.line_and_message"
msgstr "Error at {line}, {column}: {message}"
@ -216,6 +285,9 @@ msgstr "File already imported."
msgid "errors.duplicate_import"
msgstr "Duplicate import name."
msgid "errors.unknown_using"
msgstr "Unknown autoload in using statement."
msgid "errors.empty_title"
msgstr "Titles cannot be empty."
@ -223,7 +295,7 @@ msgid "errors.duplicate_title"
msgstr "There is already a title with that name."
msgid "errors.nested_title"
msgstr "Titles cannot be nested."
msgstr "Titles cannot be indented."
msgid "errors.invalid_title_string"
msgstr "Titles can only contain alphanumeric characters and numbers."
@ -259,7 +331,7 @@ msgid "errors.condition_has_no_content"
msgstr "A condition line needs an indented line below it."
msgid "errors.incomplete_expression"
msgstr "Incomplate expression."
msgstr "Incomplete expression."
msgid "errors.invalid_expression_for_value"
msgstr "Invalid expression for value."
@ -366,12 +438,6 @@ msgstr "You have {count} errors in your dialogue text. See Output for details."
msgid "runtime.invalid_expression"
msgstr "\"{expression}\" is not a valid expression: {error}"
msgid "runtime.unsupported_array_method"
msgstr "Calling \"{method_name}\" on an array isn't supported."
msgid "runtime.unsupported_dictionary_method"
msgstr "Calling \"{method_name}\" on a dictionary isn't supported."
msgid "runtime.array_index_out_of_bounds"
msgstr "Index {index} out of bounds of array \"{array}\"."
@ -384,6 +450,9 @@ msgstr "Key \"{key}\" not found in dictionary \"{dictionary}\""
msgid "runtime.property_not_found"
msgstr "\"{property}\" is not a property on any game states ({states})."
msgid "runtime.property_not_found_missing_export"
msgstr "\"{property}\" is not a property on any game states ({states}). You might need to add an [Export] decorator."
msgid "runtime.method_not_found"
msgstr "\"{method}\" is not a method on any game states ({states})"
@ -396,5 +465,17 @@ msgstr "\"{method}\" is not a callable method on \"{object}\""
msgid "runtime.unknown_operator"
msgstr "Unknown operator."
msgid "runtime.unknown_autoload"
msgstr "\"{autoload}\" doesn't appear to be a valid autoload."
msgid "runtime.something_went_wrong"
msgstr "Something went wrong."
msgstr "Something went wrong."
msgid "runtime.expected_n_got_n_args"
msgstr "\"{method}\" was called with {received} arguments but it only has {expected}."
msgid "runtime.unsupported_array_type"
msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead."
msgid "runtime.dialogue_balloon_missing_start_method"
msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method."

View File

@ -0,0 +1,457 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Dialogue Manager\n"
"POT-Creation-Date: 2024-02-25 20:58\n"
"PO-Revision-Date: 2024-02-25 20:58\n"
"Last-Translator: you <you@example.com>\n"
"Language-Team: Spanish <yourteam@example.com>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "start_a_new_file"
msgstr "Crear un nuevo archivo"
msgid "open_a_file"
msgstr "Abrir un archivo"
msgid "open.open"
msgstr "Abrir..."
msgid "open.no_recent_files"
msgstr "No hay archivos recientes"
msgid "open.clear_recent_files"
msgstr "Limpiar archivos recientes"
msgid "save_all_files"
msgstr "Guardar todos los archivos"
msgid "test_dialogue"
msgstr "Diálogo de prueba"
msgid "search_for_text"
msgstr "Buscar texto"
msgid "insert"
msgstr "Insertar"
msgid "translations"
msgstr "Traducciones"
msgid "settings"
msgstr "Ajustes"
msgid "show_support"
msgstr "Contribuye con Dialogue Manager"
msgid "docs"
msgstr "Docs"
msgid "insert.wave_bbcode"
msgstr "BBCode ondulado"
msgid "insert.shake_bbcode"
msgstr "BBCode agitado"
msgid "insert.typing_pause"
msgstr "Pausa de escritura"
msgid "insert.typing_speed_change"
msgstr "Cambiar la velocidad de escritura"
msgid "insert.auto_advance"
msgstr "Avance automático"
msgid "insert.templates"
msgstr "Plantillas"
msgid "insert.title"
msgstr "Título"
msgid "insert.dialogue"
msgstr "Diálogo"
msgid "insert.response"
msgstr "Respuesta"
msgid "insert.random_lines"
msgstr "Líneas aleatorias"
msgid "insert.random_text"
msgstr "Texto aleatorio"
msgid "insert.actions"
msgstr "Acciones"
msgid "insert.jump"
msgstr "Ir al título"
msgid "insert.end_dialogue"
msgstr "Finalizar diálogo"
msgid "generate_line_ids"
msgstr "Generar IDs de línea"
msgid "save_characters_to_csv"
msgstr "Guardar los nombres de los personajes en un archivo CSV..."
msgid "save_to_csv"
msgstr "Guardar líneas en CSV..."
msgid "import_from_csv"
msgstr "Importar cambios de línea desde CSV..."
msgid "confirm_close"
msgstr "¿Guardar los cambios en '{path}'?"
msgid "confirm_close.save"
msgstr "Guardar cambios"
msgid "confirm_close.discard"
msgstr "Descartar"
msgid "buffer.save"
msgstr "Guardar"
msgid "buffer.save_as"
msgstr "Guardar como..."
msgid "buffer.close"
msgstr "Cerrar"
msgid "buffer.close_all"
msgstr "Cerrar todo"
msgid "buffer.close_other_files"
msgstr "Cerrar otros archivos"
msgid "buffer.copy_file_path"
msgstr "Copiar la ruta del archivo"
msgid "buffer.show_in_filesystem"
msgstr "Mostrar en el sistema de archivos"
msgid "settings.invalid_test_scene"
msgstr "\"{path}\" no extiende BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Revertir a la escena de prueba por defecto"
msgid "settings.default_balloon_hint"
msgstr ""
"Globo personalizado para usar al llamar a \"DialogueManager.show_balloon()\""
msgid "settings.revert_to_default_balloon"
msgstr "Volver al globo predeterminado"
msgid "settings.default_balloon_path"
msgstr "<globo de ejemplo>"
msgid "settings.autoload"
msgstr "Autocarga"
msgid "settings.path"
msgstr "Ruta"
msgid "settings.new_template"
msgstr "Los nuevos archivos de diálogo empezarán con una plantilla"
msgid "settings.missing_keys"
msgstr "Tratar las claves de traducción faltantes como errores"
msgid "settings.missing_keys_hint"
msgstr "Si estás utilizando claves de traducción estáticas, tener esta opción habilitada te ayudará a encontrar cualquier línea a la que aún no le hayas añadido una clave."
msgid "settings.characters_translations"
msgstr "Exportar nombres de personajes en archivos de traducción"
msgid "settings.wrap_long_lines"
msgstr "Romper líneas largas"
msgid "settings.include_failed_responses"
msgstr "Incluir respuestas con condiciones fallidas"
msgid "settings.ignore_missing_state_values"
msgstr "Omitir errores de valores de estado faltantes (no recomendado)"
msgid "settings.custom_test_scene"
msgstr "Escena de prueba personalizada (debe extender BaseDialogueTestScene)"
msgid "settings.default_csv_locale"
msgstr "Localización CSV por defecto"
msgid "settings.states_shortcuts"
msgstr "Atajos de teclado"
msgid "settings.states_message"
msgstr "Si un autoload está habilitado aquí, puedes referirte a sus propiedades y métodos sin tener que usar su nombre."
msgid "settings.states_hint"
msgstr "ie. En lugar de \"SomeState.some_property\" podría simplemente usar \"some_property\""
msgid "settings.recompile_warning"
msgstr "Cambiar estos ajustes obligará a recompilar todo el diálogo. Hazlo solo si sabes lo que estás haciendo."
msgid "settings.create_lines_for_responses_with_characters"
msgstr "Crear línea de diálogo para respuestas con nombres de personajes dentro."
msgid "settings.open_in_external_editor"
msgstr "Abrir archivos de diálogo en el editor externo"
msgid "settings.external_editor_warning"
msgstr "Nota: El resaltado de sintaxis y la verificación detallada de errores no están soportados en editores externos."
msgid "settings.include_characters_in_translations"
msgstr "Incluir nombres de personajes en las exportaciones de traducción"
msgid "settings.include_notes_in_translations"
msgstr "Incluir notas (## comentarios) en las exportaciones de traducción"
msgid "n_of_n"
msgstr "{index} de {total}"
msgid "search.previous"
msgstr "Anterior"
msgid "search.next"
msgstr "Siguiente"
msgid "search.match_case"
msgstr "Coincidir mayúsculas/minúsculas"
msgid "search.toggle_replace"
msgstr "Reemplazar"
msgid "search.replace_with"
msgstr "Reemplazar con:"
msgid "search.replace"
msgstr "Reemplazar"
msgid "search.replace_all"
msgstr "Reemplazar todo"
msgid "files_list.filter"
msgstr "Filtrar archivos"
msgid "titles_list.filter"
msgstr "Filtrar títulos"
msgid "errors.key_not_found"
msgstr "La tecla \"{key}\" no se encuentra."
msgid "errors.line_and_message"
msgstr "Error en {line}, {column}: {message}"
msgid "errors_in_script"
msgstr "Tienes errores en tu guion. Corrígelos y luego inténtalo de nuevo."
msgid "errors_with_build"
msgstr "Debes corregir los errores de diálogo antes de poder ejecutar tu juego."
msgid "errors.import_errors"
msgstr "Hay errores en este archivo importado."
msgid "errors.already_imported"
msgstr "Archivo ya importado."
msgid "errors.duplicate_import"
msgstr "Nombre de importación duplicado."
msgid "errors.unknown_using"
msgstr "Autoload desconocida en la declaración de uso."
msgid "errors.empty_title"
msgstr "Los títulos no pueden estar vacíos."
msgid "errors.duplicate_title"
msgstr "Ya hay un título con ese nombre."
msgid "errors.nested_title"
msgstr "Los títulos no pueden tener sangría."
msgid "errors.invalid_title_string"
msgstr "Los títulos solo pueden contener caracteres alfanuméricos y números."
msgid "errors.invalid_title_number"
msgstr "Los títulos no pueden empezar con un número."
msgid "errors.unknown_title"
msgstr "Título desconocido."
msgid "errors.jump_to_invalid_title"
msgstr "Este salto está apuntando a un título inválido."
msgid "errors.title_has_no_content"
msgstr "Ese título no tiene contenido. Quizá cambiarlo a \"=> FIN\"."
msgid "errors.invalid_expression"
msgstr "La expresión es inválida."
msgid "errors.unexpected_condition"
msgstr "Condición inesperada."
msgid "errors.duplicate_id"
msgstr "Este ID ya está en otra línea."
msgid "errors.missing_id"
msgstr "Esta línea está sin ID."
msgid "errors.invalid_indentation"
msgstr "Sangría no válida."
msgid "errors.condition_has_no_content"
msgstr "Una línea de condición necesita una línea sangrada debajo de ella."
msgid "errors.incomplete_expression"
msgstr "Expresión incompleta."
msgid "errors.invalid_expression_for_value"
msgstr "Expresión no válida para valor."
msgid "errors.file_not_found"
msgstr "Archivo no encontrado."
msgid "errors.unexpected_end_of_expression"
msgstr "Fin de expresión inesperado."
msgid "errors.unexpected_function"
msgstr "Función inesperada."
msgid "errors.unexpected_bracket"
msgstr "Corchete inesperado."
msgid "errors.unexpected_closing_bracket"
msgstr "Bracket de cierre inesperado."
msgid "errors.missing_closing_bracket"
msgstr "Falta cerrar corchete."
msgid "errors.unexpected_operator"
msgstr "Operador inesperado."
msgid "errors.unexpected_comma"
msgstr "Coma inesperada."
msgid "errors.unexpected_colon"
msgstr "Dos puntos inesperados"
msgid "errors.unexpected_dot"
msgstr "Punto inesperado."
msgid "errors.unexpected_boolean"
msgstr "Booleano inesperado."
msgid "errors.unexpected_string"
msgstr "String inesperado."
msgid "errors.unexpected_number"
msgstr "Número inesperado."
msgid "errors.unexpected_variable"
msgstr "Variable inesperada."
msgid "errors.invalid_index"
msgstr "Índice no válido."
msgid "errors.unexpected_assignment"
msgstr "Asignación inesperada."
msgid "errors.unknown"
msgstr "Sintaxis desconocida."
msgid "update.available"
msgstr "v{version} disponible"
msgid "update.is_available_for_download"
msgstr "¡La versión %s ya está disponible para su descarga!"
msgid "update.downloading"
msgstr "Descargando..."
msgid "update.download_update"
msgstr "Descargar actualización"
msgid "update.needs_reload"
msgstr "El proyecto debe ser recargado para instalar la actualización."
msgid "update.reload_ok_button"
msgstr "Recargar proyecto"
msgid "update.reload_cancel_button"
msgstr "Hazlo más tarde"
msgid "update.reload_project"
msgstr "Recargar proyecto"
msgid "update.release_notes"
msgstr "Leer las notas de la versión"
msgid "update.success"
msgstr "El Gestor de Diálogo ahora es v{versión}."
msgid "update.failed"
msgstr "Hubo un problema al descargar la actualización."
msgid "runtime.no_resource"
msgstr "Recurso de diálogo no proporcionado."
msgid "runtime.no_content"
msgstr "\"{file_path}\" no tiene contenido."
msgid "runtime.errors"
msgstr "Tienes {count} errores en tu diálogo de texto."
msgid "runtime.error_detail"
msgstr "Línea {line}: {message}"
msgid "runtime.errors_see_details"
msgstr "Tienes {count} errores en tu texto de diálogo. Consulta la salida para más detalles."
msgid "runtime.invalid_expression"
msgstr "\"{expression}\" no es una expresión válida: {error}"
msgid "runtime.array_index_out_of_bounds"
msgstr "Índice {index} fuera de los límites del array \"{array}\"."
msgid "runtime.left_hand_size_cannot_be_assigned_to"
msgstr "El lado izquierdo de la expresión no se puede asignar."
msgid "runtime.key_not_found"
msgstr "Clave \"{key}\" no encontrada en el diccionario \"{dictionary}\""
msgid "runtime.property_not_found"
msgstr "\"{property}\" no es una propiedad en ningún estado del juego ({states})."
msgid "runtime.property_not_found_missing_export"
msgstr "\"{property}\" no es una propiedad en ningún estado del juego ({states}). Es posible que necesites añadir un decorador [Export]."
msgid "runtime.method_not_found"
msgstr "\"{method}\" no es un método en ningún estado del juego ({states})"
msgid "runtime.signal_not_found"
msgstr "\"{signal_name}\" no es una señal en ningún estado del juego ({states})"
msgid "runtime.method_not_callable"
msgstr "\"{method}\" no es un método llamable en \"{object}\""
msgid "runtime.unknown_operator"
msgstr "Operador desconocido."
msgid "runtime.unknown_autoload"
msgstr "\"{autoload}\" parece no ser un autoload válido."
msgid "runtime.something_went_wrong"
msgstr "Algo salió mal."
msgid "runtime.expected_n_got_n_args"
msgstr "El método \"{method}\" se llamó con {received} argumentos, pero solo tiene {expected}."
msgid "runtime.unsupported_array_type"
msgstr "Array[{type}] no está soportado en mutaciones. Utiliza Array como tipo en su lugar."
msgid "runtime.dialogue_balloon_missing_start_method"
msgstr "Tu globo de diálogo no tiene un método \"start\" o \"Start\"."

View File

@ -23,6 +23,9 @@ msgstr ""
msgid "save_all_files"
msgstr ""
msgid "find_in_files"
msgstr ""
msgid "test_dialogue"
msgstr ""
@ -38,6 +41,12 @@ msgstr ""
msgid "settings"
msgstr ""
msgid "sponsor"
msgstr ""
msgid "show_support"
msgstr ""
msgid "docs"
msgstr ""
@ -122,9 +131,21 @@ msgstr ""
msgid "buffer.show_in_filesystem"
msgstr ""
msgid "settings.invalid_test_scene"
msgstr ""
msgid "settings.revert_to_default_test_scene"
msgstr ""
msgid "settings.default_balloon_hint"
msgstr ""
msgid "settings.revert_to_default_balloon"
msgstr ""
msgid "settings.default_balloon_path"
msgstr ""
msgid "settings.autoload"
msgstr ""
@ -140,15 +161,24 @@ msgstr ""
msgid "settings.missing_keys_hint"
msgstr ""
msgid "settings.characters_translations"
msgstr ""
msgid "settings.wrap_long_lines"
msgstr ""
msgid "settings.include_failed_responses"
msgstr ""
msgid "settings.ignore_missing_state_values"
msgstr ""
msgid "settings.custom_test_scene"
msgstr ""
msgid "settings.default_csv_locale"
msgstr ""
msgid "settings.states_shortcuts"
msgstr ""
@ -158,9 +188,45 @@ msgstr ""
msgid "settings.states_hint"
msgstr ""
msgid "settings.recompile_warning"
msgstr ""
msgid "settings.create_lines_for_responses_with_characters"
msgstr ""
msgid "settings.open_in_external_editor"
msgstr ""
msgid "settings.external_editor_warning"
msgstr ""
msgid "settings.include_characters_in_translations"
msgstr ""
msgid "settings.include_notes_in_translations"
msgstr ""
msgid "settings.check_for_updates"
msgstr ""
msgid "n_of_n"
msgstr ""
msgid "search.find"
msgstr ""
msgid "search.find_all"
msgstr ""
msgid "search.placeholder"
msgstr ""
msgid "search.replace_placeholder"
msgstr ""
msgid "search.replace_selected"
msgstr ""
msgid "search.previous"
msgstr ""
@ -188,6 +254,9 @@ msgstr ""
msgid "titles_list.filter"
msgstr ""
msgid "errors.key_not_found"
msgstr ""
msgid "errors.line_and_message"
msgstr ""
@ -206,6 +275,9 @@ msgstr ""
msgid "errors.duplicate_import"
msgstr ""
msgid "errors.unknown_using"
msgstr ""
msgid "errors.empty_title"
msgstr ""
@ -356,12 +428,6 @@ msgstr ""
msgid "runtime.invalid_expression"
msgstr ""
msgid "runtime.unsupported_array_method"
msgstr ""
msgid "runtime.unsupported_dictionary_method"
msgstr ""
msgid "runtime.array_index_out_of_bounds"
msgstr ""
@ -374,6 +440,9 @@ msgstr ""
msgid "runtime.property_not_found"
msgstr ""
msgid "runtime.property_not_found_missing_export"
msgstr ""
msgid "runtime.method_not_found"
msgstr ""
@ -386,5 +455,17 @@ msgstr ""
msgid "runtime.unknown_operator"
msgstr ""
msgid "runtime.unknown_autoload"
msgstr ""
msgid "runtime.something_went_wrong"
msgstr ""
msgid "runtime.expected_n_got_n_args"
msgstr ""
msgid "runtime.unsupported_array_type"
msgstr ""
msgid "runtime.dialogue_balloon_missing_start_method"
msgstr ""

View File

@ -0,0 +1,408 @@
msgid ""
msgstr ""
"Project-Id-Version: Dialogue Manager\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: penghao123456、憨憨羊の宇航鸽鸽\n"
"Language: zh\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4\n"
msgid "start_a_new_file"
msgstr "创建新文件"
msgid "open_a_file"
msgstr "打开已有文件"
msgid "open.open"
msgstr "打开……"
msgid "open.no_recent_files"
msgstr "无历史记录"
msgid "open.clear_recent_files"
msgstr "清空历史记录"
msgid "save_all_files"
msgstr "保存所有文件"
msgid "test_dialogue"
msgstr "测试对话"
msgid "search_for_text"
msgstr "查找……"
msgid "insert"
msgstr "插入"
msgid "translations"
msgstr "翻译"
msgid "settings"
msgstr "设置"
msgid "show_support"
msgstr "支持 Dialogue Manager"
msgid "docs"
msgstr "文档"
msgid "insert.wave_bbcode"
msgstr "BBCode [lb]wave[rb]"
msgid "insert.shake_bbcode"
msgstr "BBCode [lb]wave[rb]"
msgid "insert.typing_pause"
msgstr "输入间隔"
msgid "insert.typing_speed_change"
msgstr "输入速度变更"
msgid "insert.auto_advance"
msgstr "自动切行"
msgid "insert.templates"
msgstr "模板"
msgid "insert.title"
msgstr "标题"
msgid "insert.dialogue"
msgstr "对话"
msgid "insert.response"
msgstr "回复选项"
msgid "insert.random_lines"
msgstr "随机行"
msgid "insert.random_text"
msgstr "随机文本"
msgid "insert.actions"
msgstr "操作"
msgid "insert.jump"
msgstr "标题间跳转"
msgid "insert.end_dialogue"
msgstr "结束对话"
msgid "generate_line_ids"
msgstr "生成行 ID"
msgid "save_to_csv"
msgstr "生成 CSV"
msgid "import_from_csv"
msgstr "从 CSV 导入"
msgid "confirm_close"
msgstr "是否要保存到“{path}”?"
msgid "confirm_close.save"
msgstr "保存"
msgid "confirm_close.discard"
msgstr "不保存"
msgid "buffer.save"
msgstr "保存"
msgid "buffer.save_as"
msgstr "另存为……"
msgid "buffer.close"
msgstr "关闭"
msgid "buffer.close_all"
msgstr "全部关闭"
msgid "buffer.close_other_files"
msgstr "关闭其他文件"
msgid "buffer.copy_file_path"
msgstr "复制文件路径"
msgid "buffer.show_in_filesystem"
msgstr "在 Godot 侧边栏中显示"
msgid "settings.revert_to_default_test_scene"
msgstr "重置测试场景设定"
msgid "settings.autoload"
msgstr "Autoload"
msgid "settings.path"
msgstr "路径"
msgid "settings.new_template"
msgstr "新建文件时自动插入模板"
msgid "settings.missing_keys"
msgstr "将翻译键缺失视为错误"
msgid "settings.missing_keys_hint"
msgstr "如果你使用静态键,这将会帮助你寻找未添加至翻译文件的键。"
msgid "settings.characters_translations"
msgstr "在翻译文件中导出角色名。"
msgid "settings.wrap_long_lines"
msgstr "自动折行"
msgid "settings.include_failed_responses"
msgstr "在判断条件失败时仍显示回复选项"
msgid "settings.ignore_missing_state_values"
msgstr "忽略全局变量缺失错误(不建议)"
msgid "settings.custom_test_scene"
msgstr "自定义测试场景必须继承自BaseDialogueTestScene"
msgid "settings.default_csv_locale"
msgstr "默认 CSV 区域格式"
msgid "settings.states_shortcuts"
msgstr "全局变量映射"
msgid "settings.states_message"
msgstr "当一个 Autoload 在这里被勾选,他的所有成员会被映射为全局变量。"
msgid "settings.states_hint"
msgstr "比如当你开启对于“Foo”的映射时你可以将“Foo.bar”简写成“bar”。"
msgid "n_of_n"
msgstr "第{index}个,共{total}个"
msgid "search.previous"
msgstr "查找上一个"
msgid "search.next"
msgstr "查找下一个"
msgid "search.match_case"
msgstr "大小写敏感"
msgid "search.toggle_replace"
msgstr "替换"
msgid "search.replace_with"
msgstr "替换为"
msgid "search.replace"
msgstr "替换"
msgid "search.replace_all"
msgstr "全部替换"
msgid "files_list.filter"
msgstr "查找文件"
msgid "titles_list.filter"
msgstr "查找标题"
msgid "errors.key_not_found"
msgstr "键“{key}”未找到"
msgid "errors.line_and_message"
msgstr "第{line}行第{colume}列发生错误:{message}"
msgid "errors_in_script"
msgstr "你的脚本中存在错误。请修复错误,然后重试。"
msgid "errors_with_build"
msgstr "请先解决 Dialogue 中的错误。"
msgid "errors.import_errors"
msgstr "被导入的文件存在问题。"
msgid "errors.already_imported"
msgstr "文件已被导入。"
msgid "errors.duplicate_import"
msgstr "导入名不能重复。"
msgid "errors.empty_title"
msgstr "标题名不能为空。"
msgid "errors.duplicate_title"
msgstr "标题名不能重复。"
msgid "errors.nested_title"
msgstr "标题不能嵌套。"
msgid "errors.invalid_title_string"
msgstr "标题名无效。"
msgid "errors.invalid_title_number"
msgstr "标题不能以数字开始。"
msgid "errors.unknown_title"
msgstr "标题未定义。"
msgid "errors.jump_to_invalid_title"
msgstr "标题名无效。"
msgid "errors.title_has_no_content"
msgstr "目标标题为空。请替换为“=> END”。"
msgid "errors.invalid_expression"
msgstr "表达式无效。"
msgid "errors.unexpected_condition"
msgstr "未知条件。"
msgid "errors.duplicate_id"
msgstr "ID 重复。"
msgid "errors.missing_id"
msgstr "ID 不存在。"
msgid "errors.invalid_indentation"
msgstr "缩进无效。"
msgid "errors.condition_has_no_content"
msgstr "条件下方不能为空。"
msgid "errors.incomplete_expression"
msgstr "不完整的表达式。"
msgid "errors.invalid_expression_for_value"
msgstr "无效的赋值表达式。"
msgid "errors.file_not_found"
msgstr "文件不存在。"
msgid "errors.unexpected_end_of_expression"
msgstr "表达式 end 不应存在。"
msgid "errors.unexpected_function"
msgstr "函数不应存在。"
msgid "errors.unexpected_bracket"
msgstr "方括号不应存在。"
msgid "errors.unexpected_closing_bracket"
msgstr "方括号不应存在。"
msgid "errors.missing_closing_bracket"
msgstr "闭方括号不存在。"
msgid "errors.unexpected_operator"
msgstr "操作符不应存在。"
msgid "errors.unexpected_comma"
msgstr "逗号不应存在。"
msgid "errors.unexpected_colon"
msgstr "冒号不应存在。"
msgid "errors.unexpected_dot"
msgstr "句号不应存在。"
msgid "errors.unexpected_boolean"
msgstr "布尔值不应存在。"
msgid "errors.unexpected_string"
msgstr "字符串不应存在。"
msgid "errors.unexpected_number"
msgstr "数字不应存在。"
msgid "errors.unexpected_variable"
msgstr "标识符不应存在。"
msgid "errors.invalid_index"
msgstr "索引无效。"
msgid "errors.unexpected_assignment"
msgstr "不应在条件判断中使用 = ,应使用 == 。"
msgid "errors.unknown"
msgstr "语法错误。"
msgid "update.available"
msgstr "v{version} 更新可用。"
msgid "update.is_available_for_download"
msgstr "v%s 已经可以下载。"
msgid "update.downloading"
msgstr "正在下载更新……"
msgid "update.download_update"
msgstr "下载"
msgid "update.needs_reload"
msgstr "需要重新加载项目以应用更新。"
msgid "update.reload_ok_button"
msgstr "重新加载"
msgid "update.reload_cancel_button"
msgstr "暂不重新加载"
msgid "update.reload_project"
msgstr "重新加载"
msgid "update.release_notes"
msgstr "查看发行注记"
msgid "update.success"
msgstr "v{version} 已成功安装并应用。"
msgid "update.failed"
msgstr "更新失败。"
msgid "runtime.no_resource"
msgstr "找不到资源。"
msgid "runtime.no_content"
msgstr "资源“{file_path}”为空。"
msgid "runtime.errors"
msgstr "文件中存在{errrors}个错误。"
msgid "runtime.error_detail"
msgstr "第{index}行:{message}"
msgid "runtime.errors_see_details"
msgstr "文件中存在{errrors}个错误。请查看调试输出。"
msgid "runtime.invalid_expression"
msgstr "表达式“{expression}”无效:{error}"
msgid "runtime.array_index_out_of_bounds"
msgstr "数组索引“{index}”越界。(数组名:“{array}”)"
msgid "runtime.left_hand_size_cannot_be_assigned_to"
msgstr "表达式左侧的变量无法被赋值。"
msgid "runtime.key_not_found"
msgstr "键“{key}”在字典“{dictionary}”中不存在。"
msgid "runtime.property_not_found"
msgstr "“{property}”不存在。(全局变量:{states}"
msgid "runtime.property_not_found_missing_export"
msgstr "“{property}”不存在。(全局变量:{states})你可能需要添加一个修饰词 [Export]。"
msgid "runtime.method_not_found"
msgstr "“{method}”不存在。(全局变量:{states}"
msgid "runtime.signal_not_found"
msgstr "“{sighal_name}”不存在。(全局变量:{states}"
msgid "runtime.method_not_callable"
msgstr "{method}不是对象“{object}”上的函数。"
msgid "runtime.unknown_operator"
msgstr "未知操作符。"
msgid "runtime.something_went_wrong"
msgstr "有什么出错了。"

View File

@ -0,0 +1,408 @@
msgid ""
msgstr ""
"Project-Id-Version: Dialogue Manager\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: 憨憨羊の宇航鴿鴿\n"
"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4\n"
msgid "start_a_new_file"
msgstr "創建新檔案"
msgid "open_a_file"
msgstr "開啟已有檔案"
msgid "open.open"
msgstr "開啟……"
msgid "open.no_recent_files"
msgstr "無歷史記錄"
msgid "open.clear_recent_files"
msgstr "清空歷史記錄"
msgid "save_all_files"
msgstr "儲存所有檔案"
msgid "test_dialogue"
msgstr "測試對話"
msgid "search_for_text"
msgstr "搜尋……"
msgid "insert"
msgstr "插入"
msgid "translations"
msgstr "翻譯"
msgid "settings"
msgstr "設定"
msgid "show_support"
msgstr "支援 Dialogue Manager"
msgid "docs"
msgstr "文檔"
msgid "insert.wave_bbcode"
msgstr "BBCode [lb]wave[rb]"
msgid "insert.shake_bbcode"
msgstr "BBCode [lb]wave[rb]"
msgid "insert.typing_pause"
msgstr "輸入間隔"
msgid "insert.typing_speed_change"
msgstr "輸入速度變更"
msgid "insert.auto_advance"
msgstr "自動切行"
msgid "insert.templates"
msgstr "模板"
msgid "insert.title"
msgstr "標題"
msgid "insert.dialogue"
msgstr "對話"
msgid "insert.response"
msgstr "回覆選項"
msgid "insert.random_lines"
msgstr "隨機行"
msgid "insert.random_text"
msgstr "隨機文本"
msgid "insert.actions"
msgstr "操作"
msgid "insert.jump"
msgstr "標題間跳轉"
msgid "insert.end_dialogue"
msgstr "結束對話"
msgid "generate_line_ids"
msgstr "生成行 ID"
msgid "save_to_csv"
msgstr "生成 CSV"
msgid "import_from_csv"
msgstr "從 CSV 匯入"
msgid "confirm_close"
msgstr "是否要儲存到“{path}”?"
msgid "confirm_close.save"
msgstr "儲存"
msgid "confirm_close.discard"
msgstr "不儲存"
msgid "buffer.save"
msgstr "儲存"
msgid "buffer.save_as"
msgstr "儲存爲……"
msgid "buffer.close"
msgstr "關閉"
msgid "buffer.close_all"
msgstr "全部關閉"
msgid "buffer.close_other_files"
msgstr "關閉其他檔案"
msgid "buffer.copy_file_path"
msgstr "複製檔案位置"
msgid "buffer.show_in_filesystem"
msgstr "在 Godot 側邊欄中顯示"
msgid "settings.revert_to_default_test_scene"
msgstr "重置測試場景設定"
msgid "settings.autoload"
msgstr "Autoload"
msgid "settings.path"
msgstr "路徑"
msgid "settings.new_template"
msgstr "新建檔案時自動插入模板"
msgid "settings.missing_keys"
msgstr "將翻譯鍵缺失視爲錯誤"
msgid "settings.missing_keys_hint"
msgstr "如果你使用靜態鍵,這將會幫助你尋找未添加至翻譯檔案的鍵。"
msgid "settings.wrap_long_lines"
msgstr "自動折行"
msgid "settings.characters_translations"
msgstr "在翻譯檔案中匯出角色名。"
msgid "settings.include_failed_responses"
msgstr "在判斷條件失敗時仍顯示回復選項"
msgid "settings.ignore_missing_state_values"
msgstr "忽略全局變量缺失錯誤(不建議)"
msgid "settings.custom_test_scene"
msgstr "自訂測試場景必須繼承自BaseDialogueTestScene"
msgid "settings.default_csv_locale"
msgstr "預設 CSV 區域格式"
msgid "settings.states_shortcuts"
msgstr "全局變量映射"
msgid "settings.states_message"
msgstr "當一個 Autoload 在這裏被勾選,他的所有成員會被映射爲全局變量。"
msgid "settings.states_hint"
msgstr "比如當你開啓對於“Foo”的映射時你可以將“Foo.bar”簡寫成“bar”。"
msgid "n_of_n"
msgstr "第{index}個,共{total}個"
msgid "search.previous"
msgstr "搜尋上一個"
msgid "search.next"
msgstr "搜尋下一個"
msgid "search.match_case"
msgstr "大小寫敏感"
msgid "search.toggle_replace"
msgstr "替換"
msgid "search.replace_with"
msgstr "替換爲"
msgid "search.replace"
msgstr "替換"
msgid "search.replace_all"
msgstr "全部替換"
msgid "files_list.filter"
msgstr "搜尋檔案"
msgid "titles_list.filter"
msgstr "搜尋標題"
msgid "errors.key_not_found"
msgstr "鍵“{key}”未找到"
msgid "errors.line_and_message"
msgstr "第{line}行第{colume}列發生錯誤:{message}"
msgid "errors_in_script"
msgstr "你的腳本中存在錯誤。請修復錯誤,然後重試。"
msgid "errors_with_build"
msgstr "請先解決 Dialogue 中的錯誤。"
msgid "errors.import_errors"
msgstr "被匯入的檔案存在問題。"
msgid "errors.already_imported"
msgstr "檔案已被匯入。"
msgid "errors.duplicate_import"
msgstr "匯入名不能重複。"
msgid "errors.empty_title"
msgstr "標題名不能爲空。"
msgid "errors.duplicate_title"
msgstr "標題名不能重複。"
msgid "errors.nested_title"
msgstr "標題不能嵌套。"
msgid "errors.invalid_title_string"
msgstr "標題名無效。"
msgid "errors.invalid_title_number"
msgstr "標題不能以數字開始。"
msgid "errors.unknown_title"
msgstr "標題未定義。"
msgid "errors.jump_to_invalid_title"
msgstr "標題名無效。"
msgid "errors.title_has_no_content"
msgstr "目標標題爲空。請替換爲“=> END”。"
msgid "errors.invalid_expression"
msgstr "表達式無效。"
msgid "errors.unexpected_condition"
msgstr "未知條件。"
msgid "errors.duplicate_id"
msgstr "ID 重複。"
msgid "errors.missing_id"
msgstr "ID 不存在。"
msgid "errors.invalid_indentation"
msgstr "縮進無效。"
msgid "errors.condition_has_no_content"
msgstr "條件下方不能爲空。"
msgid "errors.incomplete_expression"
msgstr "不完整的表達式。"
msgid "errors.invalid_expression_for_value"
msgstr "無效的賦值表達式。"
msgid "errors.file_not_found"
msgstr "檔案不存在。"
msgid "errors.unexpected_end_of_expression"
msgstr "表達式 end 不應存在。"
msgid "errors.unexpected_function"
msgstr "函數不應存在。"
msgid "errors.unexpected_bracket"
msgstr "方括號不應存在。"
msgid "errors.unexpected_closing_bracket"
msgstr "方括號不應存在。"
msgid "errors.missing_closing_bracket"
msgstr "閉方括號不存在。"
msgid "errors.unexpected_operator"
msgstr "操作符不應存在。"
msgid "errors.unexpected_comma"
msgstr "逗號不應存在。"
msgid "errors.unexpected_colon"
msgstr "冒號不應存在。"
msgid "errors.unexpected_dot"
msgstr "句號不應存在。"
msgid "errors.unexpected_boolean"
msgstr "布爾值不應存在。"
msgid "errors.unexpected_string"
msgstr "字符串不應存在。"
msgid "errors.unexpected_number"
msgstr "數字不應存在。"
msgid "errors.unexpected_variable"
msgstr "標識符不應存在。"
msgid "errors.invalid_index"
msgstr "索引無效。"
msgid "errors.unexpected_assignment"
msgstr "不應在條件判斷中使用 = ,應使用 == 。"
msgid "errors.unknown"
msgstr "語法錯誤。"
msgid "update.available"
msgstr "v{version} 更新可用。"
msgid "update.is_available_for_download"
msgstr "v%s 已經可以下載。"
msgid "update.downloading"
msgstr "正在下載更新……"
msgid "update.download_update"
msgstr "下載"
msgid "update.needs_reload"
msgstr "需要重新加載項目以套用更新。"
msgid "update.reload_ok_button"
msgstr "重新加載"
msgid "update.reload_cancel_button"
msgstr "暫不重新加載"
msgid "update.reload_project"
msgstr "重新加載"
msgid "update.release_notes"
msgstr "查看發行註記"
msgid "update.success"
msgstr "v{version} 已成功安裝並套用。"
msgid "update.failed"
msgstr "更新失敗。"
msgid "runtime.no_resource"
msgstr "找不到資源。"
msgid "runtime.no_content"
msgstr "資源“{file_path}”爲空。"
msgid "runtime.errors"
msgstr "檔案中存在{errrors}個錯誤。"
msgid "runtime.error_detail"
msgstr "第{index}行:{message}"
msgid "runtime.errors_see_details"
msgstr "檔案中存在{errrors}個錯誤。請查看調試輸出。"
msgid "runtime.invalid_expression"
msgstr "表達式“{expression}”無效:{error}"
msgid "runtime.array_index_out_of_bounds"
msgstr "數組索引“{index}”越界。(數組名:“{array}”)"
msgid "runtime.left_hand_size_cannot_be_assigned_to"
msgstr "表達式左側的變量無法被賦值。"
msgid "runtime.key_not_found"
msgstr "鍵“{key}”在字典“{dictionary}”中不存在。"
msgid "runtime.property_not_found"
msgstr "“{property}”不存在。(全局變量:{states}"
msgid "runtime.method_not_found"
msgstr "“{method}”不存在。(全局變量:{states}"
msgid "runtime.signal_not_found"
msgstr "“{sighal_name}”不存在。(全局變量:{states}"
msgid "runtime.property_not_found_missing_export"
msgstr "“{property}”不存在。(全局變量:{states})你可能需要添加一個修飾詞 [Export]。"
msgid "runtime.method_not_callable"
msgstr "{method}不是對象“{object}”上的函數。"
msgid "runtime.unknown_operator"
msgstr "未知操作符。"
msgid "runtime.something_went_wrong"
msgstr "有什麼出錯了。"

View File

@ -3,5 +3,5 @@
name="Dialogue Manager"
description="A simple but powerful branching dialogue system"
author="Nathan Hoad"
version="2.16.2"
version="2.38.0"
script="plugin.gd"

View File

@ -2,63 +2,82 @@
extends EditorPlugin
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueImportPlugin = preload("res://addons/dialogue_manager/import_plugin.gd")
const DialogueTranslationParserPlugin = preload("res://addons/dialogue_manager/editor_translation_parser_plugin.gd")
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
const MainView = preload("res://addons/dialogue_manager/views/main_view.tscn")
const DialogueConstants = preload("./constants.gd")
const DialogueImportPlugin = preload("./import_plugin.gd")
const DialogueTranslationParserPlugin = preload("./editor_translation_parser_plugin.gd")
const DialogueSettings = preload("./settings.gd")
const DialogueCache = preload("./components/dialogue_cache.gd")
const MainView = preload("./views/main_view.tscn")
var import_plugin: DialogueImportPlugin
var translation_parser_plugin: DialogueTranslationParserPlugin
var main_view
var dialogue_file_cache: Dictionary = {}
var dialogue_cache: DialogueCache
func _enter_tree() -> void:
add_autoload_singleton("DialogueManager", "res://addons/dialogue_manager/dialogue_manager.gd")
add_custom_type("DialogueLabel", "RichTextLabel", preload("res://addons/dialogue_manager/dialogue_label.gd"), _get_plugin_icon())
add_autoload_singleton("DialogueManager", get_plugin_path() + "/dialogue_manager.gd")
if Engine.is_editor_hint():
Engine.set_meta("DialogueManagerPlugin", self)
DialogueSettings.prepare()
import_plugin = DialogueImportPlugin.new()
import_plugin.editor_plugin = self
add_import_plugin(import_plugin)
translation_parser_plugin = DialogueTranslationParserPlugin.new()
add_translation_parser_plugin(translation_parser_plugin)
main_view = MainView.instantiate()
main_view.editor_plugin = self
get_editor_interface().get_editor_main_screen().add_child(main_view)
_make_visible(false)
update_dialogue_file_cache()
get_editor_interface().get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed)
dialogue_cache = DialogueCache.new()
main_view.add_child(dialogue_cache)
Engine.set_meta("DialogueCache", dialogue_cache)
_update_localization()
get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved)
get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed)
add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon)
# Prevent the project from showing as unsaved even though it was only just opened
if Engine.get_physics_frames() == 0:
var timer: Timer = Timer.new()
var suppress_unsaved_marker: Callable
suppress_unsaved_marker = func():
if Engine.get_frames_per_second() >= 10:
timer.stop()
get_editor_interface().save_all_scenes()
timer.queue_free()
timer.timeout.connect(suppress_unsaved_marker)
add_child(timer)
timer.start(0.1)
func _exit_tree() -> void:
remove_autoload_singleton("DialogueManager")
remove_custom_type("DialogueLabel")
remove_import_plugin(import_plugin)
import_plugin = null
remove_translation_parser_plugin(translation_parser_plugin)
translation_parser_plugin = null
if is_instance_valid(main_view):
main_view.queue_free()
get_editor_interface().get_resource_filesystem().filesystem_changed.disconnect(_on_filesystem_changed)
Engine.remove_meta("DialogueManagerPlugin")
Engine.remove_meta("DialogueCache")
get_editor_interface().get_file_system_dock().files_moved.disconnect(_on_files_moved)
get_editor_interface().get_file_system_dock().file_removed.disconnect(_on_file_removed)
remove_tool_menu_item("Create copy of dialogue example balloon...")
@ -76,10 +95,19 @@ func _get_plugin_name() -> String:
func _get_plugin_icon() -> Texture2D:
return load("res://addons/dialogue_manager/assets/icon.svg")
return load(get_plugin_path() + "/assets/icon.svg")
func _handles(object) -> bool:
var editor_settings: EditorSettings = get_editor_interface().get_editor_settings()
var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path")
var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != ""
if object is DialogueResource and use_external_editor and DialogueSettings.get_user_value("open_in_external_editor", false):
var project_path: String = ProjectSettings.globalize_path("res://")
var file_path: String = ProjectSettings.globalize_path(object.resource_path)
OS.create_process(external_editor, [project_path, file_path])
return false
return object is DialogueResource
@ -91,71 +119,62 @@ func _edit(object) -> void:
func _apply_changes() -> void:
if is_instance_valid(main_view):
main_view.apply_changes()
_update_localization()
func _build() -> bool:
# If this is the dotnet Godot then we need to check if the solution file exists
if ProjectSettings.has_setting("dotnet/project/solution_directory"):
var directory: String = ProjectSettings.get("dotnet/project/solution_directory")
var file_name: String = ProjectSettings.get("dotnet/project/assembly_name")
var has_dotnet_solution: bool = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name])
DialogueSettings.set_user_value("has_dotnet_solution", has_dotnet_solution)
# Ignore errors in other files if we are just running the test scene
if DialogueSettings.get_user_value("is_running_test_scene", true): return true
var can_build: bool = true
var is_first_file: bool = true
for dialogue_file in dialogue_file_cache.values():
if dialogue_file.errors.size() > 0:
# Open the first file
if is_first_file:
get_editor_interface().edit_resource(load(dialogue_file.path))
main_view.show_build_error_dialog()
is_first_file = false
push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path])
can_build = false
return can_build
if dialogue_cache != null:
var files_with_errors = dialogue_cache.get_files_with_errors()
if files_with_errors.size() > 0:
for dialogue_file in files_with_errors:
push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path])
get_editor_interface().edit_resource(load(files_with_errors[0].path))
main_view.show_build_error_dialog()
return false
return true
## Keep track of known files and their dependencies
func add_to_dialogue_file_cache(path: String, resource_path: String, parse_results: DialogueManagerParseResult) -> void:
dialogue_file_cache[path] = {
path = path,
resource_path = resource_path,
dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path),
errors = []
}
save_dialogue_cache()
recompile_dependent_files(path)
## Get the current version
func get_version() -> String:
var config: ConfigFile = ConfigFile.new()
config.load(get_plugin_path() + "/plugin.cfg")
return config.get_value("plugin", "version")
## Keep track of compile errors
func add_errors_to_dialogue_file_cache(path: String, errors: Array[Dictionary]) -> void:
if dialogue_file_cache.has(path):
dialogue_file_cache[path]["errors"] = errors
else:
dialogue_file_cache[path] = {
path = path,
errors = errors
}
save_dialogue_cache()
recompile_dependent_files(path)
## Get the current path of the plugin
func get_plugin_path() -> String:
return get_script().resource_path.get_base_dir()
## Update references to a moved file
func update_import_paths(from_path: String, to_path: String) -> void:
# Update its own reference in the cache
if dialogue_file_cache.has(from_path):
dialogue_file_cache[to_path] = dialogue_file_cache[from_path].duplicate()
dialogue_file_cache.erase(from_path)
dialogue_cache.move_file_path(from_path, to_path)
# Reopen the file if it's already open
if main_view.current_file_path == from_path:
main_view.current_file_path = ""
main_view.open_file(to_path)
if to_path == "":
main_view.close_file(from_path)
else:
main_view.current_file_path = ""
main_view.open_file(to_path)
# Update any other files that import the moved file
var dependents = dialogue_file_cache.values().filter(func(d): return from_path in d.dependencies)
var dependents = dialogue_cache.get_files_with_dependency(from_path)
for dependent in dependents:
dependent.dependencies.erase(from_path)
dependent.dependencies.remove_at(dependent.dependencies.find(from_path))
dependent.dependencies.append(to_path)
# Update the live buffer
if main_view.current_file_path == dependent.path:
main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path)
@ -164,85 +183,35 @@ func update_import_paths(from_path: String, to_path: String) -> void:
# Open the file and update the path
var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ)
var text = file.get_as_text().replace(from_path, to_path)
file.close()
file = FileAccess.open(dependent.path, FileAccess.WRITE)
file.store_string(text)
save_dialogue_cache()
file.close()
## Rebuild any files that depend on this path
func recompile_dependent_files(path: String) -> void:
# Rebuild any files that depend on this one
var dependents = dialogue_file_cache.values().filter(func(d): return path in d.dependencies)
for dependent in dependents:
if dependent.has("path") and dependent.has("resource_path"):
import_plugin.compile_file(dependent.path, dependent.resource_path, false)
func _update_localization() -> void:
var dialogue_files = dialogue_cache.get_files()
## Make sure the cache points to real files
func update_dialogue_file_cache() -> void:
var cache: Dictionary = {}
# Open our cache file if it exists
if FileAccess.file_exists(DialogueConstants.CACHE_PATH):
var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.READ)
cache = JSON.parse_string(file.get_as_text())
# Scan for dialogue files
var current_files: PackedStringArray = _get_dialogue_files_in_filesystem()
# Add any files to POT generation
# Add any new files to POT generation
var files_for_pot: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations_pot_files", [])
var files_for_pot_changed: bool = false
for path in current_files:
for path in dialogue_files:
if not files_for_pot.has(path):
files_for_pot.append(path)
files_for_pot_changed = true
# Remove any files that don't exist any more
for path in cache.keys():
if not path in current_files:
cache.erase(path)
DialogueSettings.remove_recent_file(path)
# Remove missing files from POT generation
if files_for_pot.has(path):
files_for_pot.remove_at(files_for_pot.find(path))
files_for_pot_changed = true
# Remove any POT references that don't exist any more
for i in range(files_for_pot.size() - 1, -1, -1):
var file_for_pot: String = files_for_pot[i]
if file_for_pot.get_extension() == "dialogue" and not dialogue_files.has(file_for_pot):
files_for_pot.remove_at(i)
files_for_pot_changed = true
# Update project settings if POT changed
if files_for_pot_changed:
ProjectSettings.set_setting("internationalization/locale/translations_pot_files", files_for_pot)
ProjectSettings.save()
dialogue_file_cache = cache
## Persist the cache
func save_dialogue_cache() -> void:
var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(dialogue_file_cache))
## Recursively find any dialogue files in a directory
func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray:
var files: PackedStringArray = []
if DirAccess.dir_exists_absolute(path):
var dir = DirAccess.open(path)
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
var file_path: String = (path + "/" + file_name).simplify_path()
if dir.current_is_dir():
if not file_name in [".godot", ".tmp"]:
files.append_array(_get_dialogue_files_in_filesystem(file_path))
elif file_name.get_extension() == "dialogue":
files.append(file_path)
file_name = dir.get_next()
return files
### Callbacks
@ -257,27 +226,36 @@ func _copy_dialogue_balloon() -> void:
directory_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
directory_dialog.min_size = Vector2(600, 500) * scale
directory_dialog.dir_selected.connect(func(path):
var file: FileAccess = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.tscn", FileAccess.READ)
var file_contents: String = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd")
file = FileAccess.open(path + "/balloon.tscn", FileAccess.WRITE)
var plugin_path: String = get_plugin_path()
var is_dotnet: bool = DialogueSettings.has_dotnet_solution()
var balloon_path: String = path + ("/Balloon.tscn" if is_dotnet else "/balloon.tscn")
var balloon_script_path: String = path + ("/DialogueBalloon.cs" if is_dotnet else "/balloon.gd")
# Copy the balloon scene file and change the script reference
var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400
var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn"
var example_balloon_script_file_name: String = "ExampleBalloon.cs" if is_dotnet else "example_balloon.gd"
var file: FileAccess = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_file_name, FileAccess.READ)
var file_contents: String = file.get_as_text().replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path)
file = FileAccess.open(balloon_path, FileAccess.WRITE)
file.store_string(file_contents)
file.close()
file = FileAccess.open("res://addons/dialogue_manager/example_balloon/small_example_balloon.tscn", FileAccess.READ)
file_contents = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd")
file = FileAccess.open(path + "/small_balloon.tscn", FileAccess.WRITE)
file.store_string(file_contents)
file.close()
file = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.gd", FileAccess.READ)
# Copy the script file
file = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ)
file_contents = file.get_as_text()
file = FileAccess.open(path + "/balloon.gd", FileAccess.WRITE)
if is_dotnet:
file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon")
file = FileAccess.open(balloon_script_path, FileAccess.WRITE)
file.store_string(file_contents)
file.close()
get_editor_interface().get_resource_filesystem().scan()
get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path + "/balloon.tscn")
get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", balloon_path)
DialogueSettings.set_setting("balloon_path", balloon_path)
directory_dialog.queue_free()
)
get_editor_interface().get_base_control().add_child(directory_dialog)
@ -287,16 +265,12 @@ func _copy_dialogue_balloon() -> void:
### Signals
func _on_filesystem_changed() -> void:
update_dialogue_file_cache()
func _on_files_moved(old_file: String, new_file: String) -> void:
update_import_paths(old_file, new_file)
DialogueSettings.move_recent_file(old_file, new_file)
func _on_file_removed(file: String) -> void:
recompile_dependent_files(file)
update_import_paths(file, "")
if is_instance_valid(main_view):
main_view.close_file(file)

View File

@ -2,40 +2,57 @@
extends Node
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueConstants = preload("./constants.gd")
### Editor config
const DEFAULT_SETTINGS = {
"states" = [],
"missing_translations_are_errors" = false,
"wrap_lines" = false,
"new_with_template" = true,
"include_all_responses" = false,
"custom_test_scene_path" = "res://addons/dialogue_manager/test_scene.tscn"
states = [],
missing_translations_are_errors = false,
export_characters_in_translation = true,
wrap_lines = false,
new_with_template = true,
include_all_responses = false,
ignore_missing_state_values = false,
custom_test_scene_path = preload("./test_scene.tscn").resource_path,
default_csv_locale = "en",
balloon_path = "",
create_lines_for_responses_with_characters = true,
include_character_in_translation_exports = false,
include_notes_in_translation_exports = false
}
static func prepare() -> void:
# Migrate previous keys
for key in [
"states",
"missing_translations_are_errors",
"wrap_lines",
"new_with_template",
"include_all_responses",
"states",
"missing_translations_are_errors",
"export_characters_in_translation",
"wrap_lines",
"new_with_template",
"include_all_responses",
"custom_test_scene_path"
]:
if ProjectSettings.has_setting("dialogue_manager/%s" % key):
var value = ProjectSettings.get_setting("dialogue_manager/%s" % key)
ProjectSettings.set_setting("dialogue_manager/%s" % key, null)
set_setting(key, value)
# Set up defaults
# Set up initial settings
for setting in DEFAULT_SETTINGS:
if ProjectSettings.has_setting("dialogue_manager/general/%s" % setting):
ProjectSettings.set_initial_value("dialogue_manager/general/%s" % setting, DEFAULT_SETTINGS[setting])
var setting_name: String = "dialogue_manager/general/%s" % setting
if not ProjectSettings.has_setting(setting_name):
set_setting(setting, DEFAULT_SETTINGS[setting])
ProjectSettings.set_initial_value(setting_name, DEFAULT_SETTINGS[setting])
if setting.ends_with("_path"):
ProjectSettings.add_property_info({
"name": setting_name,
"type": TYPE_STRING,
"hint": PROPERTY_HINT_FILE,
})
ProjectSettings.save()
@ -52,23 +69,36 @@ static func get_setting(key: String, default):
return default
static func get_settings(only_keys: PackedStringArray = []) -> Dictionary:
var settings: Dictionary = {}
for key in DEFAULT_SETTINGS.keys():
if only_keys.is_empty() or key in only_keys:
settings[key] = get_setting(key, DEFAULT_SETTINGS[key])
return settings
### User config
static func get_user_config() -> Dictionary:
var user_config: Dictionary = {
check_for_updates = true,
just_refreshed = null,
recent_files = [],
reopen_files = [],
most_recent_reopen_file = "",
carets = {},
run_title = "",
run_resource_path = "",
is_running_test_scene = false
is_running_test_scene = false,
has_dotnet_solution = false,
open_in_external_editor = false
}
if FileAccess.file_exists(DialogueConstants.USER_CONFIG_PATH):
var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.READ)
user_config.merge(JSON.parse_string(file.get_as_text()), true)
return user_config
@ -121,8 +151,8 @@ static func clear_recent_files() -> void:
static func set_caret(path: String, cursor: Vector2) -> void:
var carets: Dictionary = get_user_value("carets", {})
carets[path] = {
x = cursor.x,
carets[path] = {
x = cursor.x,
y = cursor.y
}
set_user_value("carets", carets)
@ -135,3 +165,20 @@ static func get_caret(path: String) -> Vector2:
return Vector2(caret.x, caret.y)
else:
return Vector2.ZERO
static func has_dotnet_solution() -> bool:
if get_user_value("has_dotnet_solution", false): return true
if ProjectSettings.has_setting("dotnet/project/solution_directory"):
var directory: String = ProjectSettings.get("dotnet/project/solution_directory")
var file_name: String = ProjectSettings.get("dotnet/project/assembly_name")
var has_dotnet_solution: bool = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name])
set_user_value("has_dotnet_solution", has_dotnet_solution)
return has_dotnet_solution
else:
var plugin_path: String = new().get_script().resource_path.get_base_dir()
if not ResourceLoader.exists(plugin_path + "/DialogueManager.cs"): return false
if load(plugin_path + "/DialogueManager.cs") == null: return false
return true

View File

@ -1,7 +1,7 @@
class_name BaseDialogueTestScene extends Node2D
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
const DialogueSettings = preload("./settings.gd")
@onready var title: String = DialogueSettings.get_user_value("run_title")
@ -13,9 +13,11 @@ func _ready():
DisplayServer.window_set_position(Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - DisplayServer.window_get_size()) * 0.5)
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DialogueManager.dialogue_ended.connect(_on_dialogue_ended)
DialogueManager.show_example_dialogue_balloon(resource, title)
# Normally you can just call DialogueManager directly but doing so before the plugin has been
# enabled in settings will throw a compiler error here so I'm using get_singleton instead.
var dialogue_manager = Engine.get_singleton("DialogueManager")
dialogue_manager.dialogue_ended.connect(_on_dialogue_ended)
dialogue_manager.show_dialogue_balloon(resource, title)
func _enter_tree() -> void:

View File

@ -0,0 +1,468 @@
extends Object
const DialogueConstants = preload("../constants.gd")
const SUPPORTED_BUILTIN_TYPES = [
TYPE_ARRAY,
TYPE_VECTOR2,
TYPE_VECTOR3,
TYPE_VECTOR4,
TYPE_DICTIONARY,
TYPE_QUATERNION,
TYPE_COLOR,
TYPE_SIGNAL
]
static var resolve_method_error: Error = OK
static func is_supported(thing) -> bool:
return typeof(thing) in SUPPORTED_BUILTIN_TYPES
static func resolve_property(builtin, property: String):
match typeof(builtin):
TYPE_ARRAY, TYPE_DICTIONARY, TYPE_QUATERNION:
return builtin[property]
# Some types have constants that we need to manually resolve
TYPE_VECTOR2:
return resolve_vector2_property(builtin, property)
TYPE_VECTOR3:
return resolve_vector3_property(builtin, property)
TYPE_VECTOR4:
return resolve_vector4_property(builtin, property)
TYPE_COLOR:
return resolve_color_property(builtin, property)
static func resolve_method(thing, method_name: String, args: Array):
resolve_method_error = OK
# Resolve static methods manually
match typeof(thing):
TYPE_VECTOR2:
match method_name:
"from_angle":
return Vector2.from_angle(args[0])
TYPE_COLOR:
match method_name:
"from_hsv":
return Color.from_hsv(args[0], args[1], args[2]) if args.size() == 3 else Color.from_hsv(args[0], args[1], args[2], args[3])
"from_ok_hsl":
return Color.from_ok_hsl(args[0], args[1], args[2]) if args.size() == 3 else Color.from_ok_hsl(args[0], args[1], args[2], args[3])
"from_rgbe9995":
return Color.from_rgbe9995(args[0])
"from_string":
return Color.from_string(args[0], args[1])
TYPE_QUATERNION:
match method_name:
"from_euler":
return Quaternion.from_euler(args[0])
# Anything else can be evaulatated automatically
var references: Array = ["thing"]
for i in range(0, args.size()):
references.append("arg%d" % i)
var expression = Expression.new()
if expression.parse("thing.%s(%s)" % [method_name, ",".join(references.slice(1))], references) != OK:
assert(false, expression.get_error_text())
var result = expression.execute([thing] + args, null, false)
if expression.has_execute_failed():
resolve_method_error = ERR_CANT_RESOLVE
return null
return result
static func has_resolve_method_failed() -> bool:
return resolve_method_error != OK
static func resolve_color_property(color: Color, property: String):
match property:
"ALICE_BLUE":
return Color.ALICE_BLUE
"ANTIQUE_WHITE":
return Color.ANTIQUE_WHITE
"AQUA":
return Color.AQUA
"AQUAMARINE":
return Color.AQUAMARINE
"AZURE":
return Color.AZURE
"BEIGE":
return Color.BEIGE
"BISQUE":
return Color.BISQUE
"BLACK":
return Color.BLACK
"BLANCHED_ALMOND":
return Color.BLANCHED_ALMOND
"BLUE":
return Color.BLUE
"BLUE_VIOLET":
return Color.BLUE_VIOLET
"BROWN":
return Color.BROWN
"BURLYWOOD":
return Color.BURLYWOOD
"CADET_BLUE":
return Color.CADET_BLUE
"CHARTREUSE":
return Color.CHARTREUSE
"CHOCOLATE":
return Color.CHOCOLATE
"CORAL":
return Color.CORAL
"CORNFLOWER_BLUE":
return Color.CORNFLOWER_BLUE
"CORNSILK":
return Color.CORNSILK
"CRIMSON":
return Color.CRIMSON
"CYAN":
return Color.CYAN
"DARK_BLUE":
return Color.DARK_BLUE
"DARK_CYAN":
return Color.DARK_CYAN
"DARK_GOLDENROD":
return Color.DARK_GOLDENROD
"DARK_GRAY":
return Color.DARK_GRAY
"DARK_GREEN":
return Color.DARK_GREEN
"DARK_KHAKI":
return Color.DARK_KHAKI
"DARK_MAGENTA":
return Color.DARK_MAGENTA
"DARK_OLIVE_GREEN":
return Color.DARK_OLIVE_GREEN
"DARK_ORANGE":
return Color.DARK_ORANGE
"DARK_ORCHID":
return Color.DARK_ORCHID
"DARK_RED":
return Color.DARK_RED
"DARK_SALMON":
return Color.DARK_SALMON
"DARK_SEA_GREEN":
return Color.DARK_SEA_GREEN
"DARK_SLATE_BLUE":
return Color.DARK_SLATE_BLUE
"DARK_SLATE_GRAY":
return Color.DARK_SLATE_GRAY
"DARK_TURQUOISE":
return Color.DARK_TURQUOISE
"DARK_VIOLET":
return Color.DARK_VIOLET
"DEEP_PINK":
return Color.DEEP_PINK
"DEEP_SKY_BLUE":
return Color.DEEP_SKY_BLUE
"DIM_GRAY":
return Color.DIM_GRAY
"DODGER_BLUE":
return Color.DODGER_BLUE
"FIREBRICK":
return Color.FIREBRICK
"FLORAL_WHITE":
return Color.FLORAL_WHITE
"FOREST_GREEN":
return Color.FOREST_GREEN
"FUCHSIA":
return Color.FUCHSIA
"GAINSBORO":
return Color.GAINSBORO
"GHOST_WHITE":
return Color.GHOST_WHITE
"GOLD":
return Color.GOLD
"GOLDENROD":
return Color.GOLDENROD
"GRAY":
return Color.GRAY
"GREEN":
return Color.GREEN
"GREEN_YELLOW":
return Color.GREEN_YELLOW
"HONEYDEW":
return Color.HONEYDEW
"HOT_PINK":
return Color.HOT_PINK
"INDIAN_RED":
return Color.INDIAN_RED
"INDIGO":
return Color.INDIGO
"IVORY":
return Color.IVORY
"KHAKI":
return Color.KHAKI
"LAVENDER":
return Color.LAVENDER
"LAVENDER_BLUSH":
return Color.LAVENDER_BLUSH
"LAWN_GREEN":
return Color.LAWN_GREEN
"LEMON_CHIFFON":
return Color.LEMON_CHIFFON
"LIGHT_BLUE":
return Color.LIGHT_BLUE
"LIGHT_CORAL":
return Color.LIGHT_CORAL
"LIGHT_CYAN":
return Color.LIGHT_CYAN
"LIGHT_GOLDENROD":
return Color.LIGHT_GOLDENROD
"LIGHT_GRAY":
return Color.LIGHT_GRAY
"LIGHT_GREEN":
return Color.LIGHT_GREEN
"LIGHT_PINK":
return Color.LIGHT_PINK
"LIGHT_SALMON":
return Color.LIGHT_SALMON
"LIGHT_SEA_GREEN":
return Color.LIGHT_SEA_GREEN
"LIGHT_SKY_BLUE":
return Color.LIGHT_SKY_BLUE
"LIGHT_SLATE_GRAY":
return Color.LIGHT_SLATE_GRAY
"LIGHT_STEEL_BLUE":
return Color.LIGHT_STEEL_BLUE
"LIGHT_YELLOW":
return Color.LIGHT_YELLOW
"LIME":
return Color.LIME
"LIME_GREEN":
return Color.LIME_GREEN
"LINEN":
return Color.LINEN
"MAGENTA":
return Color.MAGENTA
"MAROON":
return Color.MAROON
"MEDIUM_AQUAMARINE":
return Color.MEDIUM_AQUAMARINE
"MEDIUM_BLUE":
return Color.MEDIUM_BLUE
"MEDIUM_ORCHID":
return Color.MEDIUM_ORCHID
"MEDIUM_PURPLE":
return Color.MEDIUM_PURPLE
"MEDIUM_SEA_GREEN":
return Color.MEDIUM_SEA_GREEN
"MEDIUM_SLATE_BLUE":
return Color.MEDIUM_SLATE_BLUE
"MEDIUM_SPRING_GREEN":
return Color.MEDIUM_SPRING_GREEN
"MEDIUM_TURQUOISE":
return Color.MEDIUM_TURQUOISE
"MEDIUM_VIOLET_RED":
return Color.MEDIUM_VIOLET_RED
"MIDNIGHT_BLUE":
return Color.MIDNIGHT_BLUE
"MINT_CREAM":
return Color.MINT_CREAM
"MISTY_ROSE":
return Color.MISTY_ROSE
"MOCCASIN":
return Color.MOCCASIN
"NAVAJO_WHITE":
return Color.NAVAJO_WHITE
"NAVY_BLUE":
return Color.NAVY_BLUE
"OLD_LACE":
return Color.OLD_LACE
"OLIVE":
return Color.OLIVE
"OLIVE_DRAB":
return Color.OLIVE_DRAB
"ORANGE":
return Color.ORANGE
"ORANGE_RED":
return Color.ORANGE_RED
"ORCHID":
return Color.ORCHID
"PALE_GOLDENROD":
return Color.PALE_GOLDENROD
"PALE_GREEN":
return Color.PALE_GREEN
"PALE_TURQUOISE":
return Color.PALE_TURQUOISE
"PALE_VIOLET_RED":
return Color.PALE_VIOLET_RED
"PAPAYA_WHIP":
return Color.PAPAYA_WHIP
"PEACH_PUFF":
return Color.PEACH_PUFF
"PERU":
return Color.PERU
"PINK":
return Color.PINK
"PLUM":
return Color.PLUM
"POWDER_BLUE":
return Color.POWDER_BLUE
"PURPLE":
return Color.PURPLE
"REBECCA_PURPLE":
return Color.REBECCA_PURPLE
"RED":
return Color.RED
"ROSY_BROWN":
return Color.ROSY_BROWN
"ROYAL_BLUE":
return Color.ROYAL_BLUE
"SADDLE_BROWN":
return Color.SADDLE_BROWN
"SALMON":
return Color.SALMON
"SANDY_BROWN":
return Color.SANDY_BROWN
"SEA_GREEN":
return Color.SEA_GREEN
"SEASHELL":
return Color.SEASHELL
"SIENNA":
return Color.SIENNA
"SILVER":
return Color.SILVER
"SKY_BLUE":
return Color.SKY_BLUE
"SLATE_BLUE":
return Color.SLATE_BLUE
"SLATE_GRAY":
return Color.SLATE_GRAY
"SNOW":
return Color.SNOW
"SPRING_GREEN":
return Color.SPRING_GREEN
"STEEL_BLUE":
return Color.STEEL_BLUE
"TAN":
return Color.TAN
"TEAL":
return Color.TEAL
"THISTLE":
return Color.THISTLE
"TOMATO":
return Color.TOMATO
"TRANSPARENT":
return Color.TRANSPARENT
"TURQUOISE":
return Color.TURQUOISE
"VIOLET":
return Color.VIOLET
"WEB_GRAY":
return Color.WEB_GRAY
"WEB_GREEN":
return Color.WEB_GREEN
"WEB_MAROON":
return Color.WEB_MAROON
"WEB_PURPLE":
return Color.WEB_PURPLE
"WHEAT":
return Color.WHEAT
"WHITE":
return Color.WHITE
"WHITE_SMOKE":
return Color.WHITE_SMOKE
"YELLOW":
return Color.YELLOW
"YELLOW_GREEN":
return Color.YELLOW_GREEN
return color[property]
static func resolve_vector2_property(vector: Vector2, property: String):
match property:
"AXIS_X":
return Vector2.AXIS_X
"AXIS_Y":
return Vector2.AXIS_Y
"ZERO":
return Vector2.ZERO
"ONE":
return Vector2.ONE
"INF":
return Vector2.INF
"LEFT":
return Vector2.LEFT
"RIGHT":
return Vector2.RIGHT
"UP":
return Vector2.UP
"DOWN":
return Vector2.DOWN
return vector[property]
static func resolve_vector3_property(vector: Vector3, property: String):
match property:
"AXIS_X":
return Vector3.AXIS_X
"AXIS_Y":
return Vector3.AXIS_Y
"AXIS_Z":
return Vector3.AXIS_Z
"ZERO":
return Vector3.ZERO
"ONE":
return Vector3.ONE
"INF":
return Vector3.INF
"LEFT":
return Vector3.LEFT
"RIGHT":
return Vector3.RIGHT
"UP":
return Vector3.UP
"DOWN":
return Vector3.DOWN
"FORWARD":
return Vector3.FORWARD
"BACK":
return Vector3.BACK
"MODEL_LEFT":
return Vector3(1, 0, 0)
"MODEL_RIGHT":
return Vector3(-1, 0, 0)
"MODEL_TOP":
return Vector3(0, 1, 0)
"MODEL_BOTTOM":
return Vector3(0, -1, 0)
"MODEL_FRONT":
return Vector3(0, 0, 1)
"MODEL_REAR":
return Vector3(0, 0, -1)
return vector[property]
static func resolve_vector4_property(vector: Vector4, property: String):
match property:
"AXIS_X":
return Vector4.AXIS_X
"AXIS_Y":
return Vector4.AXIS_Y
"AXIS_Z":
return Vector4.AXIS_Z
"AXIS_W":
return Vector4.AXIS_W
"ZERO":
return Vector4.ZERO
"ONE":
return Vector4.ONE
"INF":
return Vector4.INF
return vector[property]

View File

@ -2,8 +2,8 @@
extends Control
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const OPEN_OPEN = 100
const OPEN_CLEAR = 101
@ -41,16 +41,20 @@ enum TranslationSource {
@onready var build_error_dialog: AcceptDialog = $BuildErrorDialog
@onready var close_confirmation_dialog: ConfirmationDialog = $CloseConfirmationDialog
@onready var updated_dialog: AcceptDialog = $UpdatedDialog
@onready var find_in_files_dialog: AcceptDialog = $FindInFilesDialog
@onready var find_in_files: Control = $FindInFilesDialog/FindInFiles
# Toolbar
@onready var new_button: Button = %NewButton
@onready var open_button: MenuButton = %OpenButton
@onready var save_all_button: Button = %SaveAllButton
@onready var find_in_files_button: Button = %FindInFilesButton
@onready var test_button: Button = %TestButton
@onready var search_button: Button = %SearchButton
@onready var insert_button: MenuButton = %InsertButton
@onready var translations_button: MenuButton = %TranslationsButton
@onready var settings_button: Button = %SettingsButton
@onready var support_button: Button = %SupportButton
@onready var docs_button: Button = %DocsButton
@onready var version_label: Label = %VersionLabel
@onready var update_button: Button = %UpdateButton
@ -83,6 +87,7 @@ var current_file_path: String = "":
files_list.hide()
title_list.hide()
code_edit.hide()
errors_panel.hide()
else:
test_button.disabled = false
search_button.disabled = false
@ -120,7 +125,7 @@ func _ready() -> void:
self.current_file_path = ""
# Set up the update checker
version_label.text = "v%s" % update_button.get_version()
version_label.text = "v%s" % editor_plugin.get_version()
update_button.editor_plugin = editor_plugin
update_button.on_before_refresh = func on_before_refresh():
# Save everything
@ -149,14 +154,27 @@ func _ready() -> void:
editor_settings.settings_changed.connect(_on_editor_settings_changed)
_on_editor_settings_changed()
# Reopen any files that were open when Godot was closed
if editor_settings.get_setting("text_editor/behavior/files/restore_scripts_on_load"):
var reopen_files: Array = DialogueSettings.get_user_value("reopen_files", [])
for reopen_file in reopen_files:
open_file(reopen_file)
self.current_file_path = DialogueSettings.get_user_value("most_recent_reopen_file", "")
save_all_button.disabled = true
close_confirmation_dialog.ok_button_text = DialogueConstants.translate("confirm_close.save")
close_confirmation_dialog.add_button(DialogueConstants.translate("confirm_close.discard"), true, "discard")
close_confirmation_dialog.ok_button_text = DialogueConstants.translate(&"confirm_close.save")
close_confirmation_dialog.add_button(DialogueConstants.translate(&"confirm_close.discard"), true, "discard")
settings_view.editor_plugin = editor_plugin
errors_dialog.dialog_text = DialogueConstants.translate("errors_in_script")
errors_dialog.dialog_text = DialogueConstants.translate(&"errors_in_script")
func _exit_tree() -> void:
DialogueSettings.set_user_value("reopen_files", open_buffers.keys())
DialogueSettings.set_user_value("most_recent_reopen_file", self.current_file_path)
func _unhandled_input(event: InputEvent) -> void:
@ -164,13 +182,18 @@ func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed():
match event.as_text():
"Ctrl+Alt+S":
"Ctrl+Alt+S", "Command+Alt+S":
get_viewport().set_input_as_handled()
save_file(current_file_path)
"Ctrl+W":
"Ctrl+W", "Command+W":
get_viewport().set_input_as_handled()
close_file(current_file_path)
"Ctrl+F5":
"Ctrl+F5", "Command+F5":
get_viewport().set_input_as_handled()
_on_test_button_pressed()
"Ctrl+Shift+F", "Command+Shift+F":
get_viewport().set_input_as_handled()
_on_find_in_files_button_pressed()
func apply_changes() -> void:
@ -195,7 +218,7 @@ func load_from_version_refresh(just_refreshed: Dictionary) -> void:
else:
editor_plugin.get_editor_interface().set_main_screen_editor("Dialogue")
updated_dialog.dialog_text = DialogueConstants.translate("update.success").format({ version = update_button.get_version() })
updated_dialog.dialog_text = DialogueConstants.translate(&"update.success").format({ version = update_button.get_version() })
updated_dialog.popup_centered()
@ -253,26 +276,29 @@ func open_file(path: String) -> void:
func show_file_in_filesystem(path: String) -> void:
var file_system = editor_plugin.get_editor_interface().get_file_system_dock()
file_system.navigate_to_path(path)
var file_system_dock: FileSystemDock = Engine.get_meta("DialogueManagerPlugin") \
.get_editor_interface() \
.get_file_system_dock()
file_system_dock.navigate_to_path(path)
# Save any open files
func save_files() -> void:
save_all_button.disabled = true
var saved_files: PackedStringArray = []
for path in open_buffers:
if open_buffers[path].text != open_buffers[path].pristine_text:
saved_files.append(path)
save_file(path)
save_file(path, false)
# Make sure we reimport/recompile the changes
if saved_files.size() > 0:
editor_plugin.get_editor_interface().get_resource_filesystem().reimport_files(saved_files)
save_all_button.disabled = true
Engine.get_meta("DialogueCache").reimport_files(saved_files)
# Save a file
func save_file(path: String) -> void:
func save_file(path: String, rescan_file_system: bool = true) -> void:
var buffer = open_buffers[path]
files_list.mark_file_as_unsaved(path, false)
@ -289,6 +315,12 @@ func save_file(path: String) -> void:
file.store_string(buffer.text)
file.close()
if rescan_file_system:
Engine.get_meta("DialogueManagerPlugin") \
.get_editor_interface() \
.get_resource_filesystem()\
.scan()
func close_file(file: String) -> void:
if not file in open_buffers.keys(): return
@ -298,7 +330,7 @@ func close_file(file: String) -> void:
if buffer.text == buffer.pristine_text:
remove_file_from_open_buffers(file)
else:
close_confirmation_dialog.dialog_text = DialogueConstants.translate("confirm_close").format({ path = file.get_file() })
close_confirmation_dialog.dialog_text = DialogueConstants.translate(&"confirm_close").format({ path = file.get_file() })
close_confirmation_dialog.popup_centered()
@ -328,6 +360,9 @@ func apply_theme() -> void:
current_line_color = editor_settings.get_setting("text_editor/theme/highlighting/current_line_color"),
error_line_color = editor_settings.get_setting("text_editor/theme/highlighting/mark_color"),
critical_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/critical_color"),
notice_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/notice_color"),
titles_color = editor_settings.get_setting("text_editor/theme/highlighting/control_flow_keyword_color"),
text_color = editor_settings.get_setting("text_editor/theme/highlighting/text_color"),
conditions_color = editor_settings.get_setting("text_editor/theme/highlighting/keyword_color"),
@ -343,69 +378,78 @@ func apply_theme() -> void:
}
new_button.icon = get_theme_icon("New", "EditorIcons")
new_button.tooltip_text = DialogueConstants.translate("start_a_new_file")
new_button.tooltip_text = DialogueConstants.translate(&"start_a_new_file")
open_button.icon = get_theme_icon("Load", "EditorIcons")
open_button.tooltip_text = DialogueConstants.translate("open_a_file")
open_button.tooltip_text = DialogueConstants.translate(&"open_a_file")
save_all_button.icon = get_theme_icon("Save", "EditorIcons")
save_all_button.tooltip_text = DialogueConstants.translate("start_all_files")
save_all_button.tooltip_text = DialogueConstants.translate(&"start_all_files")
find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons")
find_in_files_button.tooltip_text = DialogueConstants.translate(&"find_in_files")
test_button.icon = get_theme_icon("PlayScene", "EditorIcons")
test_button.tooltip_text = DialogueConstants.translate("test_dialogue")
test_button.tooltip_text = DialogueConstants.translate(&"test_dialogue")
search_button.icon = get_theme_icon("Search", "EditorIcons")
search_button.tooltip_text = DialogueConstants.translate("search_for_text")
search_button.tooltip_text = DialogueConstants.translate(&"search_for_text")
insert_button.icon = get_theme_icon("RichTextEffect", "EditorIcons")
insert_button.text = DialogueConstants.translate("insert")
insert_button.text = DialogueConstants.translate(&"insert")
translations_button.icon = get_theme_icon("Translation", "EditorIcons")
translations_button.text = DialogueConstants.translate("translations")
translations_button.text = DialogueConstants.translate(&"translations")
settings_button.icon = get_theme_icon("Tools", "EditorIcons")
settings_button.tooltip_text = DialogueConstants.translate("settings")
settings_button.tooltip_text = DialogueConstants.translate(&"settings")
support_button.icon = get_theme_icon("Heart", "EditorIcons")
support_button.text = DialogueConstants.translate(&"sponsor")
support_button.tooltip_text = DialogueConstants.translate(&"show_support")
docs_button.icon = get_theme_icon("Help", "EditorIcons")
docs_button.text = DialogueConstants.translate("docs")
docs_button.text = DialogueConstants.translate(&"docs")
update_button.apply_theme()
# Set up the effect menu
var popup: PopupMenu = insert_button.get_popup()
popup.clear()
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.wave_bbcode"), 0)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.shake_bbcode"), 1)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.wave_bbcode"), 0)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.shake_bbcode"), 1)
popup.add_separator()
popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DialogueConstants.translate("insert.typing_pause"), 3)
popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DialogueConstants.translate("insert.typing_speed_change"), 4)
popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DialogueConstants.translate("insert.auto_advance"), 5)
popup.add_separator(DialogueConstants.translate("insert.templates"))
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.title"), 6)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.dialogue"), 7)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.response"), 8)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.random_lines"), 9)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.random_text"), 10)
popup.add_separator(DialogueConstants.translate("insert.actions"))
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.jump"), 11)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.end_dialogue"), 12)
popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DialogueConstants.translate(&"insert.typing_pause"), 3)
popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DialogueConstants.translate(&"insert.typing_speed_change"), 4)
popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DialogueConstants.translate(&"insert.auto_advance"), 5)
popup.add_separator(DialogueConstants.translate(&"insert.templates"))
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.title"), 6)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.dialogue"), 7)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.response"), 8)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_lines"), 9)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_text"), 10)
popup.add_separator(DialogueConstants.translate(&"insert.actions"))
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.jump"), 11)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.end_dialogue"), 12)
# Set up the translations menu
popup = translations_button.get_popup()
popup.clear()
popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DialogueConstants.translate("generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS)
popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DialogueConstants.translate(&"generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS)
popup.add_separator()
popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate("save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV)
popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate("save_to_csv"), TRANSLATIONS_SAVE_TO_CSV)
popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DialogueConstants.translate("import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV)
popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV)
popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_to_csv"), TRANSLATIONS_SAVE_TO_CSV)
popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DialogueConstants.translate(&"import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV)
# Dialog sizes
new_dialog.min_size = Vector2(600, 500) * scale
save_dialog.min_size = Vector2(600, 500) * scale
open_dialog.min_size = Vector2(600, 500) * scale
export_dialog.min_size = Vector2(600, 500) * scale
export_dialog.min_size = Vector2(600, 500) * scale
settings_dialog.min_size = Vector2(600, 600) * scale
import_dialog.min_size = Vector2(600, 500) * scale
settings_dialog.min_size = Vector2(1000, 600) * scale
settings_dialog.max_size = Vector2(1000, 600) * scale
find_in_files_dialog.min_size = Vector2(800, 600) * scale
### Helpers
@ -415,19 +459,20 @@ func apply_theme() -> void:
func build_open_menu() -> void:
var menu = open_button.get_popup()
menu.clear()
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DialogueConstants.translate("open.open"), OPEN_OPEN)
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DialogueConstants.translate(&"open.open"), OPEN_OPEN)
menu.add_separator()
var recent_files = DialogueSettings.get_recent_files()
if recent_files.size() == 0:
menu.add_item(DialogueConstants.translate("open.no_recent_files"))
menu.add_item(DialogueConstants.translate(&"open.no_recent_files"))
menu.set_item_disabled(2, true)
else:
for path in recent_files:
menu.add_icon_item(get_theme_icon("File", "EditorIcons"), path)
if FileAccess.file_exists(path):
menu.add_icon_item(get_theme_icon("File", "EditorIcons"), path)
menu.add_separator()
menu.add_item(DialogueConstants.translate("open.clear_recent_files"), OPEN_CLEAR)
menu.add_item(DialogueConstants.translate(&"open.clear_recent_files"), OPEN_CLEAR)
if menu.id_pressed.is_connected(_on_open_menu_id_pressed):
menu.id_pressed.disconnect(_on_open_menu_id_pressed)
menu.id_pressed.connect(_on_open_menu_id_pressed)
@ -454,7 +499,7 @@ func parse() -> void:
func show_build_error_dialog() -> void:
build_error_dialog.dialog_text = DialogueConstants.translate("errors_with_build")
build_error_dialog.dialog_text = DialogueConstants.translate(&"errors_with_build")
build_error_dialog.popup_centered()
@ -531,11 +576,16 @@ func add_path_to_project_translations(path: String) -> void:
# Export dialogue and responses to CSV
func export_translations_to_csv(path: String) -> void:
var default_locale: String = DialogueSettings.get_setting("default_csv_locale", "en")
var file: FileAccess
# If the file exists, open it first and work out which keys are already in it
var existing_csv = {}
var commas = []
var existing_csv: Dictionary = {}
var column_count: int = 2
var default_locale_column: int = 1
var character_column: int = -1
var notes_column: int = -1
if FileAccess.file_exists(path):
file = FileAccess.open(path, FileAccess.READ)
var is_first_line = true
@ -544,17 +594,44 @@ func export_translations_to_csv(path: String) -> void:
line = file.get_csv_line()
if is_first_line:
is_first_line = false
for i in range(2, line.size()):
commas.append("")
column_count = line.size()
for i in range(1, line.size()):
if line[i] == default_locale:
default_locale_column = i
elif line[i] == "_character":
character_column = i
elif line[i] == "_notes":
notes_column = i
# Make sure the line isn't empty before adding it
if line.size() > 0 and line[0].strip_edges() != "":
existing_csv[line[0]] = line
# The character column wasn't found in the existing file but the setting is turned on
if character_column == -1 and DialogueSettings.get_setting("include_character_in_translation_exports", false):
character_column = column_count
column_count += 1
existing_csv["keys"].append("_character")
# The notes column wasn't found in the existing file but the setting is turned on
if notes_column == -1 and DialogueSettings.get_setting("include_notes_in_translation_exports", false):
notes_column = column_count
column_count += 1
existing_csv["keys"].append("_notes")
# Start a new file
file = FileAccess.open(path, FileAccess.WRITE)
if not file.file_exists(path):
file.store_csv_line(["keys", "en"])
if not FileAccess.file_exists(path):
var headings: PackedStringArray = ["keys", default_locale]
if DialogueSettings.get_setting("include_character_in_translation_exports", false):
character_column = headings.size()
headings.append("_character")
if DialogueSettings.get_setting("include_notes_in_translation_exports", false):
notes_column = headings.size()
headings.append("_notes")
file.store_csv_line(headings)
column_count = headings.size()
# Write our translations to file
var known_keys: PackedStringArray = []
@ -571,13 +648,22 @@ func export_translations_to_csv(path: String) -> void:
known_keys.append(line.translation_key)
var line_to_save: PackedStringArray = []
if existing_csv.has(line.translation_key):
var existing_line = existing_csv.get(line.translation_key)
existing_line[1] = line.text
lines_to_save.append(existing_line)
line_to_save = existing_csv.get(line.translation_key)
line_to_save.resize(column_count)
existing_csv.erase(line.translation_key)
else:
lines_to_save.append(PackedStringArray([line.translation_key, line.text] + commas))
line_to_save.resize(column_count)
line_to_save[0] = line.translation_key
line_to_save[default_locale_column] = line.text
if character_column > -1:
line_to_save[character_column] = "(response)" if line.type == DialogueConstants.TYPE_RESPONSE else line.character
if notes_column > -1:
line_to_save[notes_column] = line.notes
lines_to_save.append(line_to_save)
# Store lines in the file, starting with anything that already exists that hasn't been touched
for line in existing_csv.values():
@ -591,7 +677,8 @@ func export_translations_to_csv(path: String) -> void:
editor_plugin.get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path)
# Add it to the project l10n settings if it's not already there
var translation_path: String = path.replace(".csv", ".en.translation")
var language_code: RegExMatch = RegEx.create_from_string("^[a-z]{2,3}").search(default_locale)
var translation_path: String = path.replace(".csv", ".%s.translation" % language_code.get_string())
call_deferred("add_path_to_project_translations", translation_path)
@ -619,7 +706,7 @@ func export_character_names_to_csv(path: String) -> void:
file = FileAccess.open(path, FileAccess.WRITE)
if not file.file_exists(path):
file.store_csv_line(["keys", "en"])
file.store_csv_line(["keys", DialogueSettings.get_setting("default_csv_locale", "en")])
# Write our translations to file
var known_keys: PackedStringArray = []
@ -709,6 +796,15 @@ func import_translations_from_csv(path: String) -> void:
parser.free()
func show_search_form(is_enabled: bool) -> void:
if code_edit.last_selected_text:
search_and_replace.input.text = code_edit.last_selected_text
search_and_replace.visible = is_enabled
search_button.set_pressed_no_signal(is_enabled)
search_and_replace.focus_line_edit()
### Signals
@ -716,6 +812,7 @@ func _on_editor_settings_changed() -> void:
var editor_settings: EditorSettings = editor_plugin.get_editor_interface().get_editor_settings()
code_edit.minimap_draw = editor_settings.get_setting("text_editor/appearance/minimap/show_minimap")
code_edit.minimap_width = editor_settings.get_setting("text_editor/appearance/minimap/minimap_width")
code_edit.scroll_smooth = editor_settings.get_setting("text_editor/behavior/navigation/smooth_scrolling")
func _on_open_menu_id_pressed(id: int) -> void:
@ -837,11 +934,17 @@ func _on_save_all_button_pressed() -> void:
save_files()
func _on_find_in_files_button_pressed() -> void:
find_in_files_dialog.popup_centered()
find_in_files.prepare()
func _on_code_edit_text_changed() -> void:
title_list.titles = code_edit.get_titles()
var buffer = open_buffers[current_file_path]
buffer.text = code_edit.text
files_list.mark_file_as_unsaved(current_file_path, buffer.text != buffer.pristine_text)
save_all_button.disabled = open_buffers.values().filter(func(d): return d.text != d.pristine_text).size() == 0
@ -878,15 +981,11 @@ func _on_errors_panel_error_pressed(line_number: int, column_number: int) -> voi
func _on_search_button_toggled(button_pressed: bool) -> void:
if code_edit.last_selected_text:
search_and_replace.input.text = code_edit.last_selected_text
search_and_replace.visible = button_pressed
show_search_form(button_pressed)
func _on_search_and_replace_open_requested() -> void:
search_button.set_pressed_no_signal(true)
search_and_replace.visible = true
show_search_form(true)
func _on_search_and_replace_close_requested() -> void:
@ -896,6 +995,7 @@ func _on_search_and_replace_close_requested() -> void:
func _on_settings_button_pressed() -> void:
settings_view.prepare()
settings_dialog.popup_centered()
@ -905,7 +1005,7 @@ func _on_settings_view_script_button_pressed(path: String) -> void:
func _on_test_button_pressed() -> void:
apply_changes()
save_file(current_file_path)
if errors_panel.errors.size() > 0:
errors_dialog.popup_centered()
@ -918,11 +1018,16 @@ func _on_test_button_pressed() -> void:
func _on_settings_dialog_confirmed() -> void:
settings_view.apply_settings_changes()
parse()
code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DialogueSettings.get_setting("wrap_lines", false) else TextEdit.LINE_WRAPPING_NONE
code_edit.grab_focus()
func _on_support_button_pressed() -> void:
OS.shell_open("https://patreon.com/nathanhoad")
func _on_docs_button_pressed() -> void:
OS.shell_open("https://github.com/nathanhoad/godot_dialogue_manager")
@ -932,17 +1037,21 @@ func _on_files_list_file_popup_menu_requested(at_position: Vector2) -> void:
files_popup_menu.popup()
func _on_files_list_file_middle_clicked(path: String):
close_file(path)
func _on_files_popup_menu_about_to_popup() -> void:
files_popup_menu.clear()
files_popup_menu.add_item(DialogueConstants.translate("buffer.save"), ITEM_SAVE, KEY_MASK_CTRL | KEY_MASK_ALT | KEY_S)
files_popup_menu.add_item(DialogueConstants.translate("buffer.save_as"), ITEM_SAVE_AS)
files_popup_menu.add_item(DialogueConstants.translate("buffer.close"), ITEM_CLOSE, KEY_MASK_CTRL | KEY_W)
files_popup_menu.add_item(DialogueConstants.translate("buffer.close_all"), ITEM_CLOSE_ALL)
files_popup_menu.add_item(DialogueConstants.translate("buffer.close_other_files"), ITEM_CLOSE_OTHERS)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save"), ITEM_SAVE, KEY_MASK_CTRL | KEY_MASK_ALT | KEY_S)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save_as"), ITEM_SAVE_AS)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close"), ITEM_CLOSE, KEY_MASK_CTRL | KEY_W)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_all"), ITEM_CLOSE_ALL)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_other_files"), ITEM_CLOSE_OTHERS)
files_popup_menu.add_separator()
files_popup_menu.add_item(DialogueConstants.translate("buffer.copy_file_path"), ITEM_COPY_PATH)
files_popup_menu.add_item(DialogueConstants.translate("buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.copy_file_path"), ITEM_COPY_PATH)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM)
func _on_files_popup_menu_id_pressed(id: int) -> void:
@ -984,3 +1093,8 @@ func _on_close_confirmation_dialog_custom_action(action: StringName) -> void:
if action == "discard":
remove_file_from_open_buffers(current_file_path)
close_confirmation_dialog.hide()
func _on_find_in_files_result_selected(path: String, cursor: Vector2, length: int) -> void:
open_file(path)
code_edit.select(cursor.y, cursor.x, cursor.y, cursor.x + length)

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=13 format=3 uid="uid://cbuf1q3xsse3q"]
[gd_scene load_steps=14 format=3 uid="uid://cbuf1q3xsse3q"]
[ext_resource type="Script" path="res://addons/dialogue_manager/views/main_view.gd" id="1_h6qfq"]
[ext_resource type="PackedScene" uid="uid://civ6shmka5e8u" path="res://addons/dialogue_manager/components/code_edit.tscn" id="2_f73fm"]
@ -7,9 +7,11 @@
[ext_resource type="PackedScene" uid="uid://co8yl23idiwbi" path="res://addons/dialogue_manager/components/update_button.tscn" id="2_ph3vs"]
[ext_resource type="PackedScene" uid="uid://gr8nakpbrhby" path="res://addons/dialogue_manager/components/search_and_replace.tscn" id="6_ylh0t"]
[ext_resource type="PackedScene" uid="uid://cs8pwrxr5vxix" path="res://addons/dialogue_manager/components/errors_panel.tscn" id="7_5cvl4"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="7_necsa"]
[ext_resource type="PackedScene" uid="uid://cpg4lg1r3ff6m" path="res://addons/dialogue_manager/views/settings_view.tscn" id="9_8bf36"]
[ext_resource type="PackedScene" uid="uid://0n7hwviyyly4" path="res://addons/dialogue_manager/components/find_in_files.tscn" id="10_yold3"]
[sub_resource type="Image" id="Image_0jq4a"]
[sub_resource type="Image" id="Image_xvtti"]
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",
@ -19,19 +21,10 @@ data = {
}
[sub_resource type="ImageTexture" id="ImageTexture_fguub"]
image = SubResource("Image_0jq4a")
image = SubResource("Image_xvtti")
[sub_resource type="Image" id="Image_vtxr5"]
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_wbkwf"]
image = SubResource("Image_vtxr5")
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_dvnxt"]
script = ExtResource("7_necsa")
[node name="MainView" type="Control"]
layout_mode = 3
@ -77,29 +70,12 @@ layout_mode = 2
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Start a new file"
icon = SubResource("ImageTexture_fguub")
flat = true
[node name="OpenButton" type="MenuButton" parent="Margin/Content/SidePanel/Toolbar"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Open a file"
icon = SubResource("ImageTexture_fguub")
item_count = 5
popup/item_0/text = "Open..."
popup/item_0/icon = SubResource("ImageTexture_wbkwf")
popup/item_0/id = 0
popup/item_1/text = ""
popup/item_1/id = -1
popup/item_1/separator = true
popup/item_2/text = "res://examples/dialogue.dialogue"
popup/item_2/icon = SubResource("ImageTexture_wbkwf")
popup/item_2/id = 2
popup/item_3/text = ""
popup/item_3/id = -1
popup/item_3/separator = true
popup/item_4/text = "Clear recent files"
popup/item_4/id = 4
[node name="SaveAllButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"]
unique_name_in_owner = true
@ -107,6 +83,11 @@ layout_mode = 2
disabled = true
flat = true
[node name="FindInFilesButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"]
unique_name_in_owner = true
layout_mode = 2
flat = true
[node name="Bookmarks" type="VSplitContainer" parent="Margin/Content/SidePanel"]
layout_mode = 2
size_flags_vertical = 3
@ -138,33 +119,12 @@ unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "Insert"
icon = SubResource("ImageTexture_fguub")
item_count = 6
popup/item_0/text = "Wave BBCode"
popup/item_0/icon = SubResource("ImageTexture_fguub")
popup/item_0/id = 0
popup/item_1/text = "Shake BBCode"
popup/item_1/icon = SubResource("ImageTexture_fguub")
popup/item_1/id = 1
popup/item_2/text = ""
popup/item_2/id = -1
popup/item_2/separator = true
popup/item_3/text = "Typing pause"
popup/item_3/icon = SubResource("ImageTexture_fguub")
popup/item_3/id = 3
popup/item_4/text = "Typing speed change"
popup/item_4/icon = SubResource("ImageTexture_fguub")
popup/item_4/id = 4
popup/item_5/text = "Auto advance"
popup/item_5/icon = SubResource("ImageTexture_fguub")
popup/item_5/id = 5
[node name="TranslationsButton" type="MenuButton" parent="Margin/Content/CodePanel/Toolbar"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "Translations"
icon = SubResource("ImageTexture_fguub")
item_count = 6
popup/item_0/text = "Generate line IDs"
popup/item_0/icon = SubResource("ImageTexture_fguub")
@ -194,7 +154,6 @@ layout_mode = 2
tooltip_text = "Search for text"
disabled = true
toggle_mode = true
icon = SubResource("ImageTexture_fguub")
flat = true
[node name="TestButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"]
@ -202,35 +161,40 @@ unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Test dialogue"
disabled = true
icon = SubResource("ImageTexture_fguub")
flat = true
[node name="Separator3" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"]
layout_mode = 2
[node name="SettingsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Settings"
flat = true
[node name="Spacer2" type="Control" parent="Margin/Content/CodePanel/Toolbar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="SettingsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"]
[node name="SupportButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Settings"
icon = SubResource("ImageTexture_fguub")
flat = true
[node name="Separator3" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"]
[node name="Separator4" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"]
layout_mode = 2
[node name="DocsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"]
unique_name_in_owner = true
layout_mode = 2
text = "Docs"
icon = SubResource("ImageTexture_fguub")
flat = true
[node name="VersionLabel" type="Label" parent="Margin/Content/CodePanel/Toolbar"]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.490196)
layout_mode = 2
text = "v2.15.2"
text = "v2.19.0"
vertical_alignment = 1
[node name="UpdateButton" parent="Margin/Content/CodePanel/Toolbar" instance=ExtResource("2_ph3vs")]
@ -239,7 +203,6 @@ layout_mode = 2
[node name="SearchAndReplace" parent="Margin/Content/CodePanel" instance=ExtResource("6_ylh0t")]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="CodeEdit" parent="Margin/Content/CodePanel" instance=ExtResource("2_f73fm")]
@ -250,6 +213,7 @@ size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_colors/bookmark_color = Color(1, 0.333333, 0.333333, 1)
text = ""
syntax_highlighter = SubResource("SyntaxHighlighter_dvnxt")
[node name="ErrorsPanel" parent="Margin/Content/CodePanel" instance=ExtResource("7_5cvl4")]
unique_name_in_owner = true
@ -281,8 +245,11 @@ size = Vector2i(600, 500)
min_size = Vector2i(600, 500)
[node name="ImportDialog" type="FileDialog" parent="."]
title = "Open a File"
size = Vector2i(600, 500)
min_size = Vector2i(600, 500)
ok_button_text = "Open"
file_mode = 0
filters = PackedStringArray("*.csv ; Translation CSV")
[node name="ErrorsDialog" type="AcceptDialog" parent="."]
@ -292,6 +259,7 @@ dialog_text = "You have errors in your script. Fix them and then try again."
[node name="SettingsDialog" type="AcceptDialog" parent="."]
title = "Settings"
size = Vector2i(834, 600)
unresizable = true
min_size = Vector2i(600, 600)
ok_button_text = "Done"
@ -314,12 +282,28 @@ title = "Updated"
size = Vector2i(191, 100)
dialog_text = "You're now up to date!"
[node name="FindInFilesDialog" type="AcceptDialog" parent="."]
title = "Find in files"
size = Vector2i(416, 457)
ok_button_text = "Done"
[node name="FindInFiles" parent="FindInFilesDialog" node_paths=PackedStringArray("main_view", "code_edit") instance=ExtResource("10_yold3")]
custom_minimum_size = Vector2(400, 400)
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
main_view = NodePath("../..")
code_edit = NodePath("../../Margin/Content/CodePanel/CodeEdit")
[connection signal="theme_changed" from="." to="." method="_on_main_view_theme_changed"]
[connection signal="visibility_changed" from="." to="." method="_on_main_view_visibility_changed"]
[connection signal="timeout" from="ParseTimer" to="." method="_on_parse_timer_timeout"]
[connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/NewButton" to="." method="_on_new_button_pressed"]
[connection signal="about_to_popup" from="Margin/Content/SidePanel/Toolbar/OpenButton" to="." method="_on_open_button_about_to_popup"]
[connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/SaveAllButton" to="." method="_on_save_all_button_pressed"]
[connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/FindInFilesButton" to="." method="_on_find_in_files_button_pressed"]
[connection signal="file_middle_clicked" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_middle_clicked"]
[connection signal="file_popup_menu_requested" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_popup_menu_requested"]
[connection signal="file_selected" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_selected"]
[connection signal="about_to_popup" from="Margin/Content/SidePanel/Bookmarks/FilesList/FilesPopupMenu" to="." method="_on_files_popup_menu_about_to_popup"]
@ -328,6 +312,7 @@ dialog_text = "You're now up to date!"
[connection signal="toggled" from="Margin/Content/CodePanel/Toolbar/SearchButton" to="." method="_on_search_button_toggled"]
[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/TestButton" to="." method="_on_test_button_pressed"]
[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/SettingsButton" to="." method="_on_settings_button_pressed"]
[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/SupportButton" to="." method="_on_support_button_pressed"]
[connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/DocsButton" to="." method="_on_docs_button_pressed"]
[connection signal="close_requested" from="Margin/Content/CodePanel/SearchAndReplace" to="." method="_on_search_and_replace_close_requested"]
[connection signal="open_requested" from="Margin/Content/CodePanel/SearchAndReplace" to="." method="_on_search_and_replace_open_requested"]
@ -346,3 +331,4 @@ dialog_text = "You're now up to date!"
[connection signal="script_button_pressed" from="SettingsDialog/SettingsView" to="." method="_on_settings_view_script_button_pressed"]
[connection signal="confirmed" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_confirmed"]
[connection signal="custom_action" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_custom_action"]
[connection signal="result_selected" from="FindInFilesDialog/FindInFiles" to="." method="_on_find_in_files_result_selected"]

View File

@ -1,73 +1,142 @@
@tool
extends VBoxContainer
extends TabContainer
signal script_button_pressed(path: String)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const DEFAULT_TEST_SCENE_PATH = "res://addons/dialogue_manager/test_scene.tscn"
enum PathTarget {
CustomTestScene,
Balloon
}
@onready var new_template_button: CheckBox = $NewTemplateButton
@onready var missing_translations_button: CheckBox = $MissingTranslationsButton
@onready var wrap_lines_button: Button = $WrapLinesButton
@onready var test_scene_path_input: LineEdit = $CustomTestScene/TestScenePath
@onready var revert_test_scene_button: Button = $CustomTestScene/RevertTestScene
@onready var load_test_scene_button: Button = $CustomTestScene/LoadTestScene
# Editor
@onready var new_template_button: CheckBox = $Editor/NewTemplateButton
@onready var characters_translations_button: CheckBox = $Editor/CharactersTranslationsButton
@onready var wrap_lines_button: Button = $Editor/WrapLinesButton
@onready var default_csv_locale: LineEdit = $Editor/DefaultCSVLocale
# Runtime
@onready var include_all_responses_button: CheckBox = $Runtime/IncludeAllResponsesButton
@onready var ignore_missing_state_values: CheckBox = $Runtime/IgnoreMissingStateValues
@onready var balloon_path_input: LineEdit = $Runtime/CustomBalloon/BalloonPath
@onready var revert_balloon_button: Button = $Runtime/CustomBalloon/RevertBalloonPath
@onready var load_balloon_button: Button = $Runtime/CustomBalloon/LoadBalloonPath
@onready var states_title: Label = $Runtime/StatesTitle
@onready var globals_list: Tree = $Runtime/GlobalsList
# Advanced
@onready var check_for_updates: CheckBox = $Advanced/CheckForUpdates
@onready var include_characters_in_translations: CheckBox = $Advanced/IncludeCharactersInTranslations
@onready var include_notes_in_translations: CheckBox = $Advanced/IncludeNotesInTranslations
@onready var open_in_external_editor_button: CheckBox = $Advanced/OpenInExternalEditorButton
@onready var test_scene_path_input: LineEdit = $Advanced/CustomTestScene/TestScenePath
@onready var revert_test_scene_button: Button = $Advanced/CustomTestScene/RevertTestScene
@onready var load_test_scene_button: Button = $Advanced/CustomTestScene/LoadTestScene
@onready var custom_test_scene_file_dialog: FileDialog = $CustomTestSceneFileDialog
@onready var include_all_responses_button: Button = $IncludeAllResponsesButton
@onready var states_title: Label = $StatesTitle
@onready var globals_list: Tree = $GlobalsList
@onready var create_lines_for_response_characters: CheckBox = $Advanced/CreateLinesForResponseCharacters
@onready var missing_translations_button: CheckBox = $Advanced/MissingTranslationsButton
var editor_plugin: EditorPlugin
var all_globals: Dictionary = {}
var enabled_globals: Array = []
var path_target: PathTarget = PathTarget.CustomTestScene
var _default_test_scene_path: String = preload("../test_scene.tscn").resource_path
var _recompile_if_changed_settings: Dictionary
func _ready() -> void:
$NewTemplateButton.text = DialogueConstants.translate("settings.new_template")
$MissingTranslationsButton.text = DialogueConstants.translate("settings.missing_keys")
$MissingTranslationsHint.text = DialogueConstants.translate("settings.missing_keys_hint")
$WrapLinesButton.text = DialogueConstants.translate("settings.wrap_long_lines")
$IncludeAllResponsesButton.text = DialogueConstants.translate("settings.include_failed_responses")
$CustomTestSceneLabel.text = DialogueConstants.translate("settings.custom_test_scene")
$StatesTitle.text = DialogueConstants.translate("settings.states_shortcuts")
$StatesMessage.text = DialogueConstants.translate("settings.states_message")
$StatesHint.text = DialogueConstants.translate("settings.states_hint")
new_template_button.text = DialogueConstants.translate(&"settings.new_template")
$Editor/MissingTranslationsHint.text = DialogueConstants.translate(&"settings.missing_keys_hint")
characters_translations_button.text = DialogueConstants.translate(&"settings.characters_translations")
wrap_lines_button.text = DialogueConstants.translate(&"settings.wrap_long_lines")
$Editor/DefaultCSVLocaleLabel.text = DialogueConstants.translate(&"settings.default_csv_locale")
include_all_responses_button.text = DialogueConstants.translate(&"settings.include_failed_responses")
ignore_missing_state_values.text = DialogueConstants.translate(&"settings.ignore_missing_state_values")
$Runtime/CustomBalloonLabel.text = DialogueConstants.translate(&"settings.default_balloon_hint")
states_title.text = DialogueConstants.translate(&"settings.states_shortcuts")
$Runtime/StatesMessage.text = DialogueConstants.translate(&"settings.states_message")
$Runtime/StatesHint.text = DialogueConstants.translate(&"settings.states_hint")
check_for_updates.text = DialogueConstants.translate(&"settings.check_for_updates")
include_characters_in_translations.text = DialogueConstants.translate(&"settings.include_characters_in_translations")
include_notes_in_translations.text = DialogueConstants.translate(&"settings.include_notes_in_translations")
open_in_external_editor_button.text = DialogueConstants.translate(&"settings.open_in_external_editor")
$Advanced/ExternalWarning.text = DialogueConstants.translate(&"settings.external_editor_warning")
$Advanced/CustomTestSceneLabel.text = DialogueConstants.translate(&"settings.custom_test_scene")
$Advanced/RecompileWarning.text = DialogueConstants.translate(&"settings.recompile_warning")
missing_translations_button.text = DialogueConstants.translate(&"settings.missing_keys")
create_lines_for_response_characters.text = DialogueConstants.translate(&"settings.create_lines_for_responses_with_characters")
current_tab = 0
func prepare() -> void:
test_scene_path_input.placeholder_text = DialogueSettings.get_setting("custom_test_scene_path", DEFAULT_TEST_SCENE_PATH)
revert_test_scene_button.visible = test_scene_path_input.placeholder_text != DEFAULT_TEST_SCENE_PATH
_recompile_if_changed_settings = _get_settings_that_require_recompilation()
test_scene_path_input.placeholder_text = DialogueSettings.get_setting("custom_test_scene_path", _default_test_scene_path)
revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path
revert_test_scene_button.icon = get_theme_icon("RotateLeft", "EditorIcons")
revert_test_scene_button.tooltip_text = DialogueConstants.translate("settings.revert_to_default_test_scene")
revert_test_scene_button.tooltip_text = DialogueConstants.translate(&"settings.revert_to_default_test_scene")
load_test_scene_button.icon = get_theme_icon("Load", "EditorIcons")
var balloon_path: String = DialogueSettings.get_setting("balloon_path", "")
if not FileAccess.file_exists(balloon_path):
DialogueSettings.set_setting("balloon_path", "")
balloon_path = ""
balloon_path_input.placeholder_text = balloon_path if balloon_path != "" else DialogueConstants.translate(&"settings.default_balloon_path")
revert_balloon_button.visible = balloon_path != ""
revert_balloon_button.icon = get_theme_icon("RotateLeft", "EditorIcons")
revert_balloon_button.tooltip_text = DialogueConstants.translate(&"settings.revert_to_default_balloon")
load_balloon_button.icon = get_theme_icon("Load", "EditorIcons")
var scale: float = editor_plugin.get_editor_interface().get_editor_scale()
custom_test_scene_file_dialog.min_size = Vector2(600, 500) * scale
states_title.add_theme_font_override("font", get_theme_font("bold", "EditorFonts"))
missing_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("missing_translations_are_errors", false))
check_for_updates.set_pressed_no_signal(DialogueSettings.get_user_value("check_for_updates", true))
characters_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("export_characters_in_translation", true))
wrap_lines_button.set_pressed_no_signal(DialogueSettings.get_setting("wrap_lines", false))
include_all_responses_button.set_pressed_no_signal(DialogueSettings.get_setting("include_all_responses", false))
ignore_missing_state_values.set_pressed_no_signal(DialogueSettings.get_setting("ignore_missing_state_values", false))
new_template_button.set_pressed_no_signal(DialogueSettings.get_setting("new_with_template", true))
default_csv_locale.text = DialogueSettings.get_setting("default_csv_locale", "en")
missing_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("missing_translations_are_errors", false))
create_lines_for_response_characters.set_pressed_no_signal(DialogueSettings.get_setting("create_lines_for_responses_with_characters", true))
include_characters_in_translations.set_pressed_no_signal(DialogueSettings.get_setting("include_character_in_translation_exports", false))
include_notes_in_translations.set_pressed_no_signal(DialogueSettings.get_setting("include_notes_in_translation_exports", false))
open_in_external_editor_button.set_pressed_no_signal(DialogueSettings.get_user_value("open_in_external_editor", false))
var editor_settings: EditorSettings = editor_plugin.get_editor_interface().get_editor_settings()
var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path")
var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != ""
if not use_external_editor:
open_in_external_editor_button.hide()
$Advanced/ExternalWarning.hide()
$Advanced/ExternalSeparator.hide()
var project = ConfigFile.new()
var err = project.load("res://project.godot")
assert(err == OK, "Could not find the project file")
all_globals.clear()
if project.has_section("autoload"):
for key in project.get_section_keys("autoload"):
if key != "DialogueManager":
all_globals[key] = project.get_value("autoload", key)
enabled_globals = DialogueSettings.get_setting("states", [])
enabled_globals = DialogueSettings.get_setting("states", []).duplicate()
globals_list.clear()
var root = globals_list.create_item()
for name in all_globals.keys():
@ -77,46 +146,58 @@ func prepare() -> void:
item.set_text(0, name)
item.add_button(1, get_theme_icon("Edit", "EditorIcons"))
item.set_text(2, all_globals.get(name, "").replace("*res://", "res://"))
globals_list.set_column_expand(0, false)
globals_list.set_column_custom_minimum_width(0, 250)
globals_list.set_column_expand(1, false)
globals_list.set_column_custom_minimum_width(1, 40)
globals_list.set_column_titles_visible(true)
globals_list.set_column_title(0, DialogueConstants.translate("settings.autoload"))
globals_list.set_column_title(0, DialogueConstants.translate(&"settings.autoload"))
globals_list.set_column_title(1, "")
globals_list.set_column_title(2, DialogueConstants.translate("settings.path"))
globals_list.set_column_title(2, DialogueConstants.translate(&"settings.path"))
func apply_settings_changes() -> void:
if _recompile_if_changed_settings != _get_settings_that_require_recompilation():
Engine.get_meta("DialogueCache").reimport_files()
func _get_settings_that_require_recompilation() -> Dictionary:
return DialogueSettings.get_settings([
"missing_translations_are_errors",
"create_lines_for_responses_with_characters"
])
### Signals
func _on_settings_view_visibility_changed() -> void:
prepare()
func _on_missing_translations_button_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("missing_translations_are_errors", toggled_on)
func _on_missing_translations_button_toggled(button_pressed: bool) -> void:
DialogueSettings.set_setting("missing_translations_are_errors", button_pressed)
func _on_characters_translations_button_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("export_characters_in_translation", toggled_on)
func _on_wrap_lines_button_toggled(button_pressed: bool) -> void:
DialogueSettings.set_setting("wrap_lines", button_pressed)
func _on_wrap_lines_button_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("wrap_lines", toggled_on)
func _on_include_all_responses_button_toggled(button_pressed: bool) -> void:
DialogueSettings.set_setting("include_all_responses", button_pressed)
func _on_include_all_responses_button_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("include_all_responses", toggled_on)
func _on_globals_list_item_selected() -> void:
var item = globals_list.get_selected()
var is_checked = not item.is_checked(0)
var is_checked = not item.is_checked(0)
item.set_checked(0, is_checked)
if is_checked:
enabled_globals.append(item.get_text(0))
else:
enabled_globals.erase(item.get_text(0))
DialogueSettings.set_setting("states", enabled_globals)
@ -124,21 +205,76 @@ func _on_globals_list_button_clicked(item: TreeItem, column: int, id: int, mouse
emit_signal("script_button_pressed", item.get_text(2))
func _on_sample_template_toggled(button_pressed):
DialogueSettings.set_setting("new_with_template", button_pressed)
func _on_sample_template_toggled(toggled_on):
DialogueSettings.set_setting("new_with_template", toggled_on)
func _on_revert_test_scene_pressed() -> void:
DialogueSettings.set_setting("custom_test_scene_path", DEFAULT_TEST_SCENE_PATH)
test_scene_path_input.placeholder_text = DEFAULT_TEST_SCENE_PATH
revert_test_scene_button.visible = test_scene_path_input.placeholder_text != DEFAULT_TEST_SCENE_PATH
DialogueSettings.set_setting("custom_test_scene_path", _default_test_scene_path)
test_scene_path_input.placeholder_text = _default_test_scene_path
revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path
func _on_load_test_scene_pressed() -> void:
path_target = PathTarget.CustomTestScene
custom_test_scene_file_dialog.popup_centered()
func _on_custom_test_scene_file_dialog_file_selected(path: String) -> void:
DialogueSettings.set_setting("custom_test_scene_path", path)
test_scene_path_input.placeholder_text = path
revert_test_scene_button.visible = test_scene_path_input.placeholder_text != DEFAULT_TEST_SCENE_PATH
match path_target:
PathTarget.CustomTestScene:
# Check that the test scene is a subclass of BaseDialogueTestScene
var test_scene: PackedScene = load(path)
if test_scene and test_scene.instantiate() is BaseDialogueTestScene:
DialogueSettings.set_setting("custom_test_scene_path", path)
test_scene_path_input.placeholder_text = path
revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path
else:
var accept: AcceptDialog = AcceptDialog.new()
accept.dialog_text = DialogueConstants.translate(&"settings.invalid_test_scene").format({ path = path })
add_child(accept)
accept.popup_centered.call_deferred()
PathTarget.Balloon:
DialogueSettings.set_setting("balloon_path", path)
balloon_path_input.placeholder_text = path
revert_balloon_button.visible = balloon_path_input.placeholder_text != ""
func _on_ignore_missing_state_values_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("ignore_missing_state_values", toggled_on)
func _on_default_csv_locale_text_changed(new_text: String) -> void:
DialogueSettings.set_setting("default_csv_locale", new_text)
func _on_revert_balloon_path_pressed() -> void:
DialogueSettings.set_setting("balloon_path", "")
balloon_path_input.placeholder_text = DialogueConstants.translate(&"settings.default_balloon_path")
revert_balloon_button.visible = DialogueSettings.get_setting("balloon_path", "") != ""
func _on_load_balloon_path_pressed() -> void:
path_target = PathTarget.Balloon
custom_test_scene_file_dialog.popup_centered()
func _on_create_lines_for_response_characters_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("create_lines_for_responses_with_characters", toggled_on)
func _on_open_in_external_editor_button_toggled(toggled_on: bool) -> void:
DialogueSettings.set_user_value("open_in_external_editor", toggled_on)
func _on_include_characters_in_translations_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("include_character_in_translation_exports", toggled_on)
func _on_include_notes_in_translations_toggled(toggled_on: bool) -> void:
DialogueSettings.set_setting("include_notes_in_translation_exports", toggled_on)
func _on_keep_up_to_date_toggled(toggled_on: bool) -> void:
DialogueSettings.set_user_value("check_for_updates", toggled_on)

File diff suppressed because one or more lines are too long