dialogue manager addon

godot-4.1.1
HumanoidSandvichDispenser 2023-07-27 11:39:38 -07:00
parent 8ef18b0ae1
commit 942a17981c
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
46 changed files with 10883 additions and 0 deletions

View File

@ -0,0 +1,144 @@
using Godot;
using Godot.Collections;
using System.Threading.Tasks;
namespace DialogueManagerRuntime
{
public partial class DialogueManager : Node
{
public static async Task<DialogueLine> GetNextDialogueLine(Resource dialogueResource, string key = "0", Array<Variant> extraGameStates = null)
{
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");
if ((RefCounted)result[0] == null) return null;
return new DialogueLine((RefCounted)result[0]);
}
public static void ShowExampleDialogueBalloon(Resource dialogueResource, string key = "0", Array<Variant> extraGameStates = null)
{
Engine.GetSingleton("DialogueManager").Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
}
public partial class DialogueLine : RefCounted
{
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();
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 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");
}
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-present Nathan Hoad
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3lr2uas6ax8v"
path="res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"
metadata={
"editor_dark_theme": true,
"editor_scale": 1.0,
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/dialogue_manager/assets/icon.svg"
dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3baj6rygkb3f"
path="res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/dialogue_manager/assets/update.svg"
dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,439 @@
@tool
extends CodeEdit
signal active_title_change(title: String)
signal error_clicked(line_number: int)
signal external_file_requested(path: String, title: String)
# A link back to the owner MainView
var main_view
# Theme overrides for syntax highlighting, etc
var theme_overrides: Dictionary:
set(value):
theme_overrides = value
syntax_highlighter.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)
# 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)
add_theme_font_override("font", get_theme_font("source", "EditorFonts"))
add_theme_font_size_override("font_size", theme_overrides.font_size * theme_overrides.scale)
font_size = round(theme_overrides.font_size)
get:
return theme_overrides
# Any parse errors
var errors: Array:
set(next_errors):
errors = next_errors
for i in range(0, get_line_count()):
var is_error: bool = false
for error in errors:
if error.line_number == i:
is_error = true
mark_line_as_error(i, is_error)
_on_code_edit_caret_changed()
get:
return errors
# The last selection (if there was one) so we can remember it for refocusing
var last_selected_text: String
var font_size: int:
set(value):
font_size = value
add_theme_font_size_override("font_size", font_size * theme_overrides.scale)
get:
return font_size
func _ready() -> void:
# Add error gutter
add_gutter(0)
set_gutter_type(0, TextEdit.GUTTER_TYPE_ICON)
# Add comment delimiter
if not has_comment_delimiter("#"):
add_comment_delimiter("#", "", true)
func _gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed():
match event.as_text():
"Ctrl+Equal", "Command+Equal":
self.font_size += 1
get_viewport().set_input_as_handled()
"Ctrl+Minus", "Command+Minus":
self.font_size -= 1
get_viewport().set_input_as_handled()
"Ctrl+0", "Command+0":
self.font_size = theme_overrides.font_size
get_viewport().set_input_as_handled()
"Ctrl+K", "Command+K":
toggle_comment()
get_viewport().set_input_as_handled()
"Alt+Up":
move_line(-1)
get_viewport().set_input_as_handled()
"Alt+Down":
move_line(1)
get_viewport().set_input_as_handled()
elif event is InputEventMouse:
match event.as_text():
"Ctrl+Mouse Wheel Up", "Command+Mouse Wheel Up":
self.font_size += 1
get_viewport().set_input_as_handled()
"Ctrl+Mouse Wheel Down", "Command+Mouse Wheel Down":
self.font_size -= 1
get_viewport().set_input_as_handled()
func _can_drop_data(at_position: Vector2, data) -> bool:
if typeof(data) != TYPE_DICTIONARY: return false
if data.type != "files": return false
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
return files.size() > 0
func _drop_data(at_position: Vector2, data) -> void:
var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+")
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
for file in files:
# Don't import the file into itself
if file == main_view.current_file_path: continue
var path = file.replace("res://", "").replace(".dialogue", "")
# Find the first non-import line in the file to add our import
var lines = text.split("\n")
for i in range(0, lines.size()):
if not lines[i].begins_with("import "):
insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)])
set_caret_line(i)
break
func _request_code_completion(force: bool) -> void:
var cursor: Vector2 = get_cursor()
var current_line: String = get_line(cursor.y)
if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")):
var prompt: String = current_line.split("=>")[1]
if prompt.begins_with("< "):
prompt = prompt.substr(2)
else:
prompt = prompt.substr(1)
if "=> " in current_line:
if matches_prompt(prompt, "end"):
add_code_completion_option(CodeEdit.KIND_CLASS, "END", "END".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
if matches_prompt(prompt, "end!"):
add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
# Get all titles, including those in imports
var parser: DialogueManagerParser = DialogueManagerParser.new()
parser.prepare(text, main_view.current_file_path, false)
for title in parser.titles:
if "/" in title:
var bits = title.split("/")
if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons"))
elif matches_prompt(prompt, title):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons"))
update_code_completion_options(true)
parser.free()
return
# var last_character: String = current_line.substr(cursor.x - 1, 1)
var name_so_far: String = current_line.strip_edges()
if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]:
# Only show names starting with that character
var names: PackedStringArray = get_character_names(name_so_far)
if names.size() > 0:
for name in names:
add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons"))
update_code_completion_options(true)
else:
cancel_code_completion()
func _filter_code_completion_candidates(candidates: Array) -> Array:
# Not sure why but if this method isn't overridden then all completions are wrapped in quotes.
return candidates
func _confirm_code_completion(replace: bool) -> void:
var completion = get_code_completion_option(get_code_completion_selected_index())
begin_complex_operation()
# Delete any part of the text that we've already typed
for i in range(0, completion.display_text.length() - completion.insert_text.length()):
backspace()
# Insert the whole match
insert_text_at_caret(completion.display_text)
end_complex_operation()
# Close the autocomplete menu on the next tick
call_deferred("cancel_code_completion")
### Helpers
# Get the current caret as a Vector2
func get_cursor() -> Vector2:
return Vector2(get_caret_column(), get_caret_line())
# Set the caret from a Vector2
func set_cursor(from_cursor: Vector2) -> void:
set_caret_line(from_cursor.y)
set_caret_column(from_cursor.x)
# Check if a prompt is the start of a string without actually being that string
func matches_prompt(prompt: String, matcher: String) -> bool:
return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower())
## Get a list of titles from the current text
func get_titles() -> PackedStringArray:
var titles = PackedStringArray([])
var lines = text.split("\n")
for line in lines:
if line.begins_with("~ "):
titles.append(line.substr(2).strip_edges())
return titles
## Work out what the next title above the current line is
func check_active_title() -> void:
var line_number = get_caret_line()
var lines = text.split("\n")
# Look at each line above this one to find the next title line
for i in range(line_number, -1, -1):
if lines[i].begins_with("~ "):
emit_signal("active_title_change", lines[i].replace("~ ", ""))
return
emit_signal("active_title_change", "0")
# Move the caret line to match a given title
func go_to_title(title: String) -> void:
var lines = text.split("\n")
for i in range(0, lines.size()):
if lines[i].strip_edges() == "~ " + title:
set_caret_line(i)
center_viewport_to_caret()
func get_character_names(beginning_with: String) -> PackedStringArray:
var names: PackedStringArray = []
var lines = text.split("\n")
for line in lines:
if ": " in line:
var name: String = line.split(": ")[0].strip_edges()
if not name in names and matches_prompt(beginning_with, name):
names.append(name)
return names
# Mark a line as an error or not
func mark_line_as_error(line_number: int, is_error: bool) -> void:
if is_error:
set_line_background_color(line_number, theme_overrides.error_line_color)
set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons"))
else:
set_line_background_color(line_number, theme_overrides.background_color)
set_line_gutter_icon(line_number, 0, null)
# Insert or wrap some bbcode at the caret/selection
func insert_bbcode(open_tag: String, close_tag: String = "") -> void:
if close_tag == "":
insert_text_at_caret(open_tag)
grab_focus()
else:
var selected_text = get_selected_text()
insert_text_at_caret("%s%s%s" % [open_tag, selected_text, close_tag])
grab_focus()
set_caret_column(get_caret_column() - close_tag.length())
# Insert text at current caret position
# Move Caret down 1 line if not => END
func insert_text(text: String) -> void:
if text != "=> END":
insert_text_at_caret(text+"\n")
set_caret_line(get_caret_line()+1)
else:
insert_text_at_caret(text)
grab_focus()
# Toggle the selected lines as comments
func toggle_comment() -> void:
# 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 = {}
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
if has_selection(caret_index):
from = get_selection_from_line(caret_index)
to = get_selection_to_line(caret_index)
for line in range(from, to + 1):
if line not in caret_offsets:
caret_offsets[line] = 0
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()
emit_signal("lines_edited_from", from, to)
# 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)
set_caret_column(get_caret_column(caret_index) + caret_offsets[get_caret_line(caret_index)], true, caret_index)
end_complex_operation()
emit_signal("text_set")
emit_signal("text_changed")
# Move the selected lines up or down
func move_line(offset: int) -> void:
offset = clamp(offset, -1, 1)
var cursor = get_cursor()
var reselect: bool = false
var from: int = cursor.y
var to: int = cursor.y
if has_selection():
reselect = true
from = get_selection_from_line()
to = get_selection_to_line()
var lines := text.split("\n")
# We can't move the lines out of bounds
if from + offset < 0 or to + offset >= lines.size(): return
var target_from_index = from - 1 if offset == -1 else to + 1
var target_to_index = to if offset == -1 else from
var line_to_move = lines[target_from_index]
lines.remove_at(target_from_index)
lines.insert(target_to_index, line_to_move)
text = "\n".join(lines)
cursor.y += offset
from += offset
to += offset
if reselect:
select(from, 0, to, get_line_width(to))
set_cursor(cursor)
emit_signal("text_changed")
### Signals
func _on_code_edit_symbol_validate(symbol: String) -> void:
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
set_symbol_lookup_word_as_valid(true)
return
for title in get_titles():
if symbol == title:
set_symbol_lookup_word_as_valid(true)
return
set_symbol_lookup_word_as_valid(false)
func _on_code_edit_symbol_lookup(symbol: String, line: int, column: int) -> void:
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
emit_signal("external_file_requested", symbol, "")
else:
go_to_title(symbol)
func _on_code_edit_text_changed() -> void:
request_code_completion(true)
func _on_code_edit_text_set() -> void:
queue_redraw()
func _on_code_edit_caret_changed() -> void:
check_active_title()
last_selected_text = get_selected_text()
func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void:
var line_errors = errors.filter(func(error): return error.line_number == line)
if line_errors.size() > 0:
emit_signal("error_clicked", line)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,85 @@
@tool
extends Control
signal failed()
signal updated(updated_to_version: String)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const TEMP_FILE_NAME = "user://temp.zip"
@onready var logo: TextureRect = %Logo
@onready var label: Label = $VBox/Label
@onready var http_request: HTTPRequest = $HTTPRequest
@onready var download_button: Button = %DownloadButton
var next_version_release: Dictionary:
set(value):
next_version_release = value
label.text = DialogueConstants.translate("update.is_available_for_download") % value.tag_name.substr(1)
get:
return next_version_release
func _ready() -> void:
$VBox/Center/DownloadButton.text = DialogueConstants.translate("update.download_update")
$VBox/Center2/NotesButton.text = DialogueConstants.translate("update.release_notes")
### Signals
func _on_download_button_pressed() -> void:
# Safeguard the actual dialogue manager repo from accidentally updating itself
if FileAccess.file_exists("res://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")
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS:
failed.emit()
return
# Save the downloaded zip
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
zip_file.store_buffer(body)
zip_file.close()
if DirAccess.dir_exists_absolute("res://addons/dialogue_manager"):
DirAccess.remove_absolute("res://addons/dialogue_manager")
var zip_reader: ZIPReader = ZIPReader.new()
zip_reader.open(TEMP_FILE_NAME)
var files: PackedStringArray = zip_reader.get_files()
var base_path = files[1]
# Remove archive folder
files.remove_at(0)
# Remove assets folder
files.remove_at(0)
for path in files:
var new_file_path: String = path.replace(base_path, "")
if path.ends_with("/"):
DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path)
else:
var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE)
file.store_buffer(zip_reader.read_file(path))
zip_reader.close()
DirAccess.remove_absolute(TEMP_FILE_NAME)
updated.emit(next_version_release.tag_name.substr(1))
func _on_notes_button_pressed() -> void:
OS.shell_open(next_version_release.html_url)

View File

@ -0,0 +1,60 @@
[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"]
[ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"]
[node name="DownloadUpdatePanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_4tm1k")
[node name="HTTPRequest" type="HTTPRequest" parent="."]
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -1.0
offset_top = 9.0
offset_right = -1.0
offset_bottom = 9.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 10
[node name="Logo" type="TextureRect" parent="VBox"]
unique_name_in_owner = true
clip_contents = true
custom_minimum_size = Vector2(300, 80)
layout_mode = 2
texture = ExtResource("2_4o2m6")
stretch_mode = 5
[node name="Label" type="Label" parent="VBox"]
layout_mode = 2
text = "v1.2.3 is available for download."
horizontal_alignment = 1
[node name="Center" type="CenterContainer" parent="VBox"]
layout_mode = 2
[node name="DownloadButton" type="Button" parent="VBox/Center"]
unique_name_in_owner = true
layout_mode = 2
text = "Download and install update"
[node name="Center2" type="CenterContainer" parent="VBox"]
layout_mode = 2
[node name="NotesButton" type="LinkButton" parent="VBox/Center2"]
layout_mode = 2
text = "Read release notes..."
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
[connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"]
[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"]

View File

@ -0,0 +1,83 @@
@tool
extends HBoxContainer
signal error_pressed(line_number)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
@onready var error_button: Button = $ErrorButton
@onready var next_button: Button = $NextButton
@onready var count_label: Label = $CountLabel
@onready var previous_button: Button = $PreviousButton
## The index of the current error being shown
var error_index: int = 0:
set(next_error_index):
error_index = wrap(next_error_index, 0, errors.size())
show_error()
get:
return error_index
## The list of all errors
var errors: Array = []:
set(next_errors):
errors = next_errors
self.error_index = 0
get:
return errors
func _ready() -> void:
apply_theme()
hide()
## Set up colors and icons
func apply_theme() -> void:
error_button.add_theme_color_override("font_color", get_theme_color("error_color", "Editor"))
error_button.add_theme_color_override("font_hover_color", get_theme_color("error_color", "Editor"))
error_button.icon = get_theme_icon("StatusError", "EditorIcons")
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
## Move the error index to match a given line
func show_error_for_line_number(line_number: int) -> void:
for i in range(0, errors.size()):
if errors[i].line_number == line_number:
self.error_index = i
## Show the current error
func show_error() -> void:
if errors.size() == 0:
hide()
else:
show()
count_label.text = DialogueConstants.translate("n_of_n").format({ index = error_index + 1, total = errors.size() })
var error = errors[error_index]
error_button.text = DialogueConstants.translate("errors.line_and_message").format({ line = error.line_number + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
### Signals
func _on_errors_panel_theme_changed() -> void:
apply_theme()
func _on_error_button_pressed() -> void:
emit_signal("error_pressed", errors[error_index].line_number, errors[error_index].column_number)
func _on_previous_button_pressed() -> void:
self.error_index -= 1
_on_error_button_pressed()
func _on_next_button_pressed() -> void:
self.error_index += 1
_on_error_button_pressed()

View File

@ -0,0 +1,56 @@
[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"]
[sub_resource type="Image" id="Image_wy5pj"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"]
image = SubResource("Image_wy5pj")
[node name="ErrorsPanel" type="HBoxContainer"]
visible = false
offset_right = 1024.0
offset_bottom = 600.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_nfm3c")
metadata/_edit_layout_mode = 1
[node name="ErrorButton" type="Button" parent="."]
layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(0, 0, 0, 1)
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
theme_override_constants/h_separation = 3
icon = SubResource("ImageTexture_s6fxl")
flat = true
alignment = 0
text_overrun_behavior = 4
[node name="Spacer" type="Control" parent="."]
custom_minimum_size = Vector2(40, 0)
layout_mode = 2
[node name="PreviousButton" type="Button" parent="."]
layout_mode = 2
icon = SubResource("ImageTexture_s6fxl")
flat = true
[node name="CountLabel" type="Label" parent="."]
layout_mode = 2
[node name="NextButton" type="Button" parent="."]
layout_mode = 2
icon = SubResource("ImageTexture_s6fxl")
flat = true
[connection signal="pressed" from="ErrorButton" to="." method="_on_error_button_pressed"]
[connection signal="pressed" from="PreviousButton" to="." method="_on_previous_button_pressed"]
[connection signal="pressed" from="NextButton" to="." method="_on_next_button_pressed"]

View File

@ -0,0 +1,135 @@
@tool
extends VBoxContainer
signal file_selected(file_path: String)
signal file_popup_menu_requested(at_position: Vector2)
signal file_double_clicked(file_path: String)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const MODIFIED_SUFFIX = "(*)"
@onready var filter_edit: LineEdit = $FilterEdit
@onready var list: ItemList = $List
var file_map: Dictionary = {}
var current_file_path: String = ""
var files: PackedStringArray = []:
set(next_files):
files = next_files
files.sort()
update_file_map()
apply_filter()
get:
return files
var unsaved_files: Array[String] = []
var filter: String:
set(next_filter):
filter = next_filter
apply_filter()
get:
return filter
func _ready() -> void:
apply_theme()
filter_edit.placeholder_text = DialogueConstants.translate("files_list.filter")
func select_file(file: String) -> void:
list.deselect_all()
for i in range(0, list.get_item_count()):
var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "")
if item_text == get_nice_file(file, item_text.count("/") + 1):
list.select(i)
func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void:
if not file in unsaved_files and is_unsaved:
unsaved_files.append(file)
elif file in unsaved_files and not is_unsaved:
unsaved_files.erase(file)
apply_filter()
func update_file_map() -> void:
file_map = {}
for file in files:
var nice_file: String = get_nice_file(file)
# See if a value with just the file name is already in the map
for key in file_map.keys():
if file_map[key] == nice_file:
var bit_count = nice_file.count("/") + 2
var existing_nice_file = get_nice_file(key, bit_count)
nice_file = get_nice_file(file, bit_count)
while nice_file == existing_nice_file:
bit_count += 1
existing_nice_file = get_nice_file(key, bit_count)
nice_file = get_nice_file(file, bit_count)
file_map[key] = existing_nice_file
file_map[file] = nice_file
func get_nice_file(file_path: String, path_bit_count: int = 1) -> String:
var bits = file_path.replace("res://", "").replace(".dialogue", "").split("/")
bits = bits.slice(-path_bit_count)
return "/".join(bits)
func apply_filter() -> void:
list.clear()
for file in file_map.keys():
if filter == "" or filter.to_lower() in file.to_lower():
var nice_file = file_map[file]
if file in unsaved_files:
nice_file += MODIFIED_SUFFIX
list.add_item(nice_file)
select_file(current_file_path)
func apply_theme() -> void:
if is_instance_valid(filter_edit):
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
### Signals
func _on_theme_changed() -> void:
apply_theme()
func _on_filter_edit_text_changed(new_text: String) -> void:
self.filter = new_text
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
if mouse_button_index == MOUSE_BUTTON_LEFT:
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
var file = file_map.find_key(item_text)
select_file(file)
file_selected.emit(file)
if mouse_button_index == MOUSE_BUTTON_RIGHT:
file_popup_menu_requested.emit(at_position)
func _on_list_item_activated(index: int) -> void:
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
var file = file_map.find_key(item_text)
select_file(file)
file_double_clicked.emit(file)

View File

@ -0,0 +1,38 @@
[gd_scene load_steps=4 format=3 uid="uid://dnufpcdrreva3"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"]
[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="FilesList" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_cytii")
[node name="FilterEdit" type="LineEdit" parent="."]
layout_mode = 2
placeholder_text = "Filter files"
clear_button_enabled = true
right_icon = SubResource("ImageTexture_wy68i")
[node name="List" type="ItemList" parent="."]
layout_mode = 2
size_flags_vertical = 3
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
[connection signal="item_activated" from="List" to="." method="_on_list_item_activated"]
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,196 @@
@tool
extends VBoxContainer
signal open_requested()
signal close_requested()
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
@onready var input: LineEdit = $Search/Input
@onready var result_label: Label = $Search/ResultLabel
@onready var previous_button: Button = $Search/PreviousButton
@onready var next_button: Button = $Search/NextButton
@onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox
@onready var replace_panel: HBoxContainer = $Replace
@onready var replace_input: LineEdit = $Replace/Input
@onready var replace_button: Button = $Replace/ReplaceButton
@onready var replace_all_button: Button = $Replace/ReplaceAllButton
# The code edit we will be affecting (for some reason exporting this didn't work)
var code_edit: CodeEdit:
set(next_code_edit):
code_edit = next_code_edit
code_edit.gui_input.connect(_on_text_edit_gui_input)
code_edit.text_changed.connect(_on_text_edit_text_changed)
get:
return code_edit
var results: Array = []
var result_index: int = -1:
set(next_result_index):
result_index = next_result_index
if results.size() > 0:
var r = results[result_index]
code_edit.set_caret_line(r[0])
code_edit.select(r[0], r[1], r[0], r[1] + r[2])
else:
result_index = -1
if is_instance_valid(code_edit):
code_edit.deselect()
result_label.text = DialogueConstants.translate("n_of_n").format({ index = result_index + 1, total = results.size() })
get:
return result_index
func _ready() -> void:
apply_theme()
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 apply_theme() -> void:
if is_instance_valid(previous_button):
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
if is_instance_valid(next_button):
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
# Find text in the code
func search(text: String = "", default_result_index: int = 0) -> void:
results.clear()
if text == "":
text = input.text
var lines = code_edit.text.split("\n")
for line_number in range(0, lines.size()):
var line = lines[line_number]
var column = find_in_line(line, text, 0)
while column > -1:
results.append([line_number, column, text.length()])
column = find_in_line(line, text, column + 1)
if results.size() > 0:
replace_button.disabled = false
replace_all_button.disabled = false
else:
replace_button.disabled = true
replace_all_button.disabled = true
self.result_index = clamp(default_result_index, 0, results.size() - 1)
# Find text in a string and match case if requested
func find_in_line(line: String, text: String, from_index: int = 0) -> int:
if match_case_button.button_pressed:
return line.find(text, from_index)
else:
return line.findn(text, from_index)
### Signals
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")
func _on_text_edit_text_changed() -> void:
results.clear()
func _on_search_and_replace_theme_changed() -> void:
apply_theme()
func _on_input_text_changed(new_text: String) -> void:
search(new_text)
func _on_previous_button_pressed() -> void:
self.result_index = wrapi(result_index - 1, 0, results.size())
func _on_next_button_pressed() -> void:
self.result_index = wrapi(result_index + 1, 0, results.size())
func _on_search_and_replace_visibility_changed() -> void:
if is_instance_valid(input):
if visible:
input.grab_focus()
var selection = code_edit.get_selected_text()
if input.text == "" and selection != "":
input.text = selection
search(selection)
else:
search()
else:
input.text = ""
func _on_input_gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed():
match event.as_text():
"Enter":
search(input.text)
"Escape":
emit_signal("close_requested")
func _on_replace_button_pressed() -> void:
if result_index == -1: return
# Replace the selection at result index
var r: Array = results[result_index]
var lines: PackedStringArray = code_edit.text.split("\n")
var line: String = lines[r[0]]
line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2])
lines[r[0]] = line
code_edit.text = "\n".join(lines)
search(input.text, result_index)
func _on_replace_all_button_pressed() -> void:
if match_case_button.button_pressed:
code_edit.text = code_edit.text.replace(input.text, replace_input.text)
else:
code_edit.text = code_edit.text.replacen(input.text, replace_input.text)
search()
func _on_replace_check_button_toggled(button_pressed: bool) -> void:
replace_panel.visible = button_pressed
if button_pressed:
replace_input.grab_focus()
func _on_input_focus_entered() -> void:
if results.size() == 0:
search()
else:
self.result_index = result_index
func _on_match_case_check_box_toggled(button_pressed: bool) -> void:
search()

View File

@ -0,0 +1,97 @@
[gd_scene load_steps=4 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"]
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 31.0
grow_horizontal = 2
size_flags_horizontal = 3
script = ExtResource("1_8oj1f")
[node name="Search" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Input" type="LineEdit" parent="Search"]
layout_mode = 2
size_flags_horizontal = 3
metadata/_edit_use_custom_anchors = true
[node name="MatchCaseCheckBox" type="CheckBox" parent="Search"]
layout_mode = 2
text = "Match case"
[node name="VSeparator" type="VSeparator" parent="Search"]
layout_mode = 2
[node name="PreviousButton" type="Button" parent="Search"]
layout_mode = 2
icon = SubResource("ImageTexture_wy68i")
flat = true
[node name="ResultLabel" type="Label" parent="Search"]
layout_mode = 2
text = "0 of 0"
[node name="NextButton" type="Button" parent="Search"]
layout_mode = 2
icon = SubResource("ImageTexture_wy68i")
flat = true
[node name="VSeparator2" type="VSeparator" parent="Search"]
layout_mode = 2
[node name="ReplaceCheckButton" type="CheckButton" parent="Search"]
layout_mode = 2
text = "Replace"
[node name="Replace" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="ReplaceLabel" type="Label" parent="Replace"]
layout_mode = 2
text = "Replace with:"
[node name="Input" type="LineEdit" parent="Replace"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ReplaceButton" type="Button" parent="Replace"]
layout_mode = 2
disabled = true
text = "Replace"
flat = true
[node name="ReplaceAllButton" type="Button" parent="Replace"]
layout_mode = 2
disabled = true
text = "Replace All"
flat = true
[connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"]
[connection signal="visibility_changed" from="." to="." method="_on_search_and_replace_visibility_changed"]
[connection signal="focus_entered" from="Search/Input" to="." method="_on_input_focus_entered"]
[connection signal="gui_input" from="Search/Input" to="." method="_on_input_gui_input"]
[connection signal="text_changed" from="Search/Input" to="." method="_on_input_text_changed"]
[connection signal="toggled" from="Search/MatchCaseCheckBox" to="." method="_on_match_case_check_box_toggled"]
[connection signal="pressed" from="Search/PreviousButton" to="." method="_on_previous_button_pressed"]
[connection signal="pressed" from="Search/NextButton" to="." method="_on_next_button_pressed"]
[connection signal="toggled" from="Search/ReplaceCheckButton" to="." method="_on_replace_check_button_toggled"]
[connection signal="focus_entered" from="Replace/Input" to="." method="_on_input_focus_entered"]
[connection signal="gui_input" from="Replace/Input" to="." method="_on_input_gui_input"]
[connection signal="pressed" from="Replace/ReplaceButton" to="." method="_on_replace_button_pressed"]
[connection signal="pressed" from="Replace/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]

View File

@ -0,0 +1,137 @@
@tool
extends Node
const DialogueConstants = preload("res://addons/dialogue_manager/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"
}
static func prepare() -> void:
# Migrate previous keys
for key in [
"states",
"missing_translations_are_errors",
"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
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])
ProjectSettings.save()
static func set_setting(key: String, value) -> void:
ProjectSettings.set_setting("dialogue_manager/general/%s" % key, value)
ProjectSettings.set_initial_value("dialogue_manager/general/%s" % key, DEFAULT_SETTINGS[key])
ProjectSettings.save()
static func get_setting(key: String, default):
if ProjectSettings.has_setting("dialogue_manager/general/%s" % key):
return ProjectSettings.get_setting("dialogue_manager/general/%s" % key)
else:
return default
### User config
static func get_user_config() -> Dictionary:
var user_config: Dictionary = {
just_refreshed = null,
recent_files = [],
carets = {},
run_title = "",
run_resource_path = "",
is_running_test_scene = 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
static func save_user_config(user_config: Dictionary) -> void:
var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(user_config))
static func set_user_value(key: String, value) -> void:
var user_config: Dictionary = get_user_config()
user_config[key] = value
save_user_config(user_config)
static func get_user_value(key: String, default = null):
return get_user_config().get(key, default)
static func add_recent_file(path: String) -> void:
var recent_files: Array = get_user_value("recent_files", [])
if path in recent_files:
recent_files.erase(path)
recent_files.insert(0, path)
set_user_value("recent_files", recent_files)
static func move_recent_file(from_path: String, to_path: String) -> void:
var recent_files: Array = get_user_value("recent_files", [])
for i in range(0, recent_files.size()):
if recent_files[i] == from_path:
recent_files[i] = to_path
set_user_value("recent_files", recent_files)
static func remove_recent_file(path: String) -> void:
var recent_files: Array = get_user_value("recent_files", [])
if path in recent_files:
recent_files.erase(path)
set_user_value("recent_files", recent_files)
static func get_recent_files() -> Array:
return get_user_value("recent_files", [])
static func clear_recent_files() -> void:
set_user_value("recent_files", [])
set_user_value("carets", {})
static func set_caret(path: String, cursor: Vector2) -> void:
var carets: Dictionary = get_user_value("carets", {})
carets[path] = {
x = cursor.x,
y = cursor.y
}
set_user_value("carets", carets)
static func get_caret(path: String) -> Vector2:
var carets = get_user_value("carets", {})
if carets.has(path):
var caret = carets.get(path)
return Vector2(caret.x, caret.y)
else:
return Vector2.ZERO

View File

@ -0,0 +1,67 @@
@tool
extends VBoxContainer
signal title_selected(title: String)
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
@onready var filter_edit: LineEdit = $FilterEdit
@onready var list: ItemList = $List
var titles: PackedStringArray:
set(next_titles):
titles = next_titles
apply_filter()
get:
return titles
var filter: String:
set(next_filter):
filter = next_filter
apply_filter()
get:
return filter
func _ready() -> void:
apply_theme()
filter_edit.placeholder_text = DialogueConstants.translate("titles_list.filter")
func select_title(title: String) -> void:
list.deselect_all()
for i in range(0, list.get_item_count()):
if list.get_item_text(i) == title.strip_edges():
list.select(i)
func apply_filter() -> void:
list.clear()
for title in titles:
if filter == "" or filter.to_lower() in title.to_lower():
list.add_item(title.strip_edges())
func apply_theme() -> void:
if is_instance_valid(filter_edit):
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
### Signals
func _on_theme_changed() -> void:
apply_theme()
func _on_filter_edit_text_changed(new_text: String) -> void:
self.filter = new_text
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
if mouse_button_index == MOUSE_BUTTON_LEFT:
var title = list.get_item_text(index)
title_selected.emit(title)

View File

@ -0,0 +1,45 @@
[gd_scene load_steps=4 format=3 uid="uid://ctns6ouwwd68i"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"]
[sub_resource type="Image" id="Image_o5dqs"]
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_ekmpw"]
image = SubResource("Image_o5dqs")
[node name="TitleList" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_5qqmd")
[node name="FilterEdit" type="LineEdit" parent="."]
layout_mode = 2
offset_right = 1152.0
offset_bottom = 31.0
placeholder_text = "Filter titles"
clear_button_enabled = true
right_icon = SubResource("ImageTexture_ekmpw")
[node name="List" type="ItemList" parent="."]
layout_mode = 2
offset_top = 35.0
offset_right = 1152.0
offset_bottom = 648.0
size_flags_vertical = 3
allow_reselect = true
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]

View File

@ -0,0 +1,132 @@
@tool
extends Button
const DialogueConstants = preload("res://addons/dialogue_manager/constants.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
@onready var download_dialog: AcceptDialog = $DownloadDialog
@onready var download_update_panel = $DownloadDialog/DownloadUpdatePanel
@onready var needs_reload_dialog: AcceptDialog = $NeedsReloadDialog
@onready var update_failed_dialog: AcceptDialog = $UpdateFailedDialog
@onready var timer: Timer = $Timer
# The main editor plugin
var editor_plugin: EditorPlugin
var needs_reload: bool = false
# A lambda that gets called just before refreshing the plugin. Return false to stop the reload.
var on_before_refresh: Callable = func(): return true
func _ready() -> void:
hide()
apply_theme()
# Check for updates on GitHub
check_for_update()
# Check again every few hours
timer.start(60 * 60 * 12)
# 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(".")
return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int()
func apply_theme() -> void:
var color: Color = get_theme_color("success_color", "Editor")
if needs_reload:
color = get_theme_color("error_color", "Editor")
icon = get_theme_icon("Reload", "EditorIcons")
add_theme_color_override("icon_normal_color", color)
add_theme_color_override("icon_focus_color", color)
add_theme_color_override("icon_hover_color", color)
add_theme_color_override("font_color", color)
add_theme_color_override("font_focus_color", color)
add_theme_color_override("font_hover_color", color)
func check_for_update() -> void:
http_request.request(REMOTE_RELEASES_URL)
### Signals
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS: return
var current_version: String = get_version()
# Work out the next version from the releases information on GitHub
var response = JSON.parse_string(body.get_string_from_utf8())
if typeof(response) != TYPE_ARRAY: return
# GitHub releases are in order of creation, not order of version
var versions = (response as Array).filter(func(release):
var version: String = release.tag_name.substr(1)
return version_to_number(version) > version_to_number(current_version)
)
if versions.size() > 0:
download_update_panel.next_version_release = versions[0]
text = DialogueConstants.translate("update.available").format({ version = versions[0].tag_name.substr(1) })
show()
func _on_update_button_pressed() -> void:
if needs_reload:
var will_refresh = on_before_refresh.call()
if will_refresh:
editor_plugin.get_editor_interface().restart_editor(true)
else:
var scale: float = editor_plugin.get_editor_interface().get_editor_scale()
download_dialog.min_size = Vector2(300, 250) * scale
download_dialog.popup_centered()
func _on_download_dialog_close_requested() -> void:
download_dialog.hide()
func _on_download_update_panel_updated(updated_to_version: String) -> void:
download_dialog.hide()
needs_reload_dialog.dialog_text = DialogueConstants.translate("update.needs_reload")
needs_reload_dialog.ok_button_text = DialogueConstants.translate("update.reload_ok_button")
needs_reload_dialog.cancel_button_text = DialogueConstants.translate("update.reload_cancel_button")
needs_reload_dialog.popup_centered()
needs_reload = true
text = DialogueConstants.translate("update.reload_project")
apply_theme()
func _on_download_update_panel_failed() -> void:
download_dialog.hide()
update_failed_dialog.dialog_text = DialogueConstants.translate("update.failed")
update_failed_dialog.popup_centered()
func _on_needs_reload_dialog_confirmed() -> void:
editor_plugin.get_editor_interface().restart_editor(true)
func _on_timer_timeout() -> void:
if not needs_reload:
check_for_update()

View File

@ -0,0 +1,42 @@
[gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"]
[ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"]
[node name="UpdateButton" type="Button"]
visible = false
offset_right = 8.0
offset_bottom = 8.0
theme_override_colors/font_color = Color(0, 0, 0, 1)
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
theme_override_colors/font_focus_color = Color(0, 0, 0, 1)
text = "v2.9.0 available"
flat = true
script = ExtResource("1_d2tpb")
[node name="HTTPRequest" type="HTTPRequest" parent="."]
[node name="DownloadDialog" type="AcceptDialog" parent="."]
title = "Download update"
size = Vector2i(400, 300)
unresizable = true
min_size = Vector2i(300, 250)
ok_button_text = "Close"
[node name="DownloadUpdatePanel" parent="DownloadDialog" instance=ExtResource("2_iwm7r")]
[node name="UpdateFailedDialog" type="AcceptDialog" parent="."]
dialog_text = "You have been updated to version 2.4.3"
[node name="NeedsReloadDialog" type="ConfirmationDialog" parent="."]
[node name="Timer" type="Timer" parent="."]
wait_time = 14400.0
[connection signal="pressed" from="." to="." method="_on_update_button_pressed"]
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
[connection signal="close_requested" from="DownloadDialog" to="." method="_on_download_dialog_close_requested"]
[connection signal="failed" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_failed"]
[connection signal="updated" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_updated"]
[connection signal="confirmed" from="NeedsReloadDialog" to="." method="_on_needs_reload_dialog_confirmed"]
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]

View File

@ -0,0 +1,181 @@
extends Node
const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json"
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_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_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!"
# Errors
const ERR_ERRORS_IN_IMPORTED_FILE = 100
const ERR_FILE_ALREADY_IMPORTED = 101
const ERR_DUPLICATE_IMPORT_NAME = 102
const ERR_EMPTY_TITLE = 103
const ERR_DUPLICATE_TITLE = 104
const ERR_NESTED_TITLE = 105
const ERR_TITLE_INVALID_CHARACTERS = 106
const ERR_UNKNOWN_TITLE = 107
const ERR_INVALID_TITLE_REFERENCE = 108
const ERR_TITLE_REFERENCE_HAS_NO_CONTENT = 109
const ERR_INVALID_EXPRESSION = 110
const ERR_UNEXPECTED_CONDITION = 111
const ERR_DUPLICATE_ID = 112
const ERR_MISSING_ID = 113
const ERR_INVALID_INDENTATION = 114
const ERR_INVALID_CONDITION_INDENTATION = 115
const ERR_INCOMPLETE_EXPRESSION = 116
const ERR_INVALID_EXPRESSION_FOR_VALUE = 117
const ERR_UNKNOWN_LINE_SYNTAX = 118
const ERR_TITLE_BEGINS_WITH_NUMBER = 119
const ERR_UNEXPECTED_END_OF_EXPRESSION = 120
const ERR_UNEXPECTED_FUNCTION = 121
const ERR_UNEXPECTED_BRACKET = 122
const ERR_UNEXPECTED_CLOSING_BRACKET = 123
const ERR_MISSING_CLOSING_BRACKET = 124
const ERR_UNEXPECTED_OPERATOR = 125
const ERR_UNEXPECTED_COMMA = 126
const ERR_UNEXPECTED_COLON = 127
const ERR_UNEXPECTED_DOT = 128
const ERR_UNEXPECTED_BOOLEAN = 129
const ERR_UNEXPECTED_STRING = 130
const ERR_UNEXPECTED_NUMBER = 131
const ERR_UNEXPECTED_VARIABLE = 132
const ERR_INVALID_INDEX = 133
const ERR_UNEXPECTED_ASSIGNMENT = 134
## Get the error message
static func get_error_message(error: int) -> String:
match error:
ERR_ERRORS_IN_IMPORTED_FILE:
return translate("errors.import_errors")
ERR_FILE_ALREADY_IMPORTED:
return translate("errors.already_imported")
ERR_DUPLICATE_IMPORT_NAME:
return translate("errors.duplicate_import")
ERR_EMPTY_TITLE:
return translate("errors.empty_title")
ERR_DUPLICATE_TITLE:
return translate("errors.duplicate_title")
ERR_NESTED_TITLE:
return translate("errors.nested_title")
ERR_TITLE_INVALID_CHARACTERS:
return translate("errors.invalid_title_string")
ERR_TITLE_BEGINS_WITH_NUMBER:
return translate("errors.invalid_title_number")
ERR_UNKNOWN_TITLE:
return translate("errors.unknown_title")
ERR_INVALID_TITLE_REFERENCE:
return translate("errors.jump_to_invalid_title")
ERR_TITLE_REFERENCE_HAS_NO_CONTENT:
return translate("errors.title_has_no_content")
ERR_INVALID_EXPRESSION:
return translate("errors.invalid_expression")
ERR_UNEXPECTED_CONDITION:
return translate("errors.unexpected_condition")
ERR_DUPLICATE_ID:
return translate("errors.duplicate_id")
ERR_MISSING_ID:
return translate("errors.missing_id")
ERR_INVALID_INDENTATION:
return translate("errors.invalid_indentation")
ERR_INVALID_CONDITION_INDENTATION:
return translate("errors.condition_has_no_content")
ERR_INCOMPLETE_EXPRESSION:
return translate("errors.incomplete_expression")
ERR_INVALID_EXPRESSION_FOR_VALUE:
return translate("errors.invalid_expression_for_value")
ERR_FILE_NOT_FOUND:
return translate("errors.file_not_found")
ERR_UNEXPECTED_END_OF_EXPRESSION:
return translate("errors.unexpected_end_of_expression")
ERR_UNEXPECTED_FUNCTION:
return translate("errors.unexpected_function")
ERR_UNEXPECTED_BRACKET:
return translate("errors.unexpected_bracket")
ERR_UNEXPECTED_CLOSING_BRACKET:
return translate("errors.unexpected_closing_bracket")
ERR_MISSING_CLOSING_BRACKET:
return translate("errors.missing_closing_bracket")
ERR_UNEXPECTED_OPERATOR:
return translate("errors.unexpected_operator")
ERR_UNEXPECTED_COMMA:
return translate("errors.unexpected_comma")
ERR_UNEXPECTED_COLON:
return translate("errors.unexpected_colon")
ERR_UNEXPECTED_DOT:
return translate("errors.unexpected_dot")
ERR_UNEXPECTED_BOOLEAN:
return translate("errors.unexpected_boolean")
ERR_UNEXPECTED_STRING:
return translate("errors.unexpected_string")
ERR_UNEXPECTED_NUMBER:
return translate("errors.unexpected_number")
ERR_UNEXPECTED_VARIABLE:
return translate("errors.unexpected_variable")
ERR_INVALID_INDEX:
return translate("errors.invalid_index")
ERR_UNEXPECTED_ASSIGNMENT:
return translate("errors.unexpected_assignment")
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)
return translations.get_message(string)

View File

@ -0,0 +1,139 @@
extends RichTextLabel
signal spoke(letter: String, letter_index: int, speed: float)
signal paused_typing(duration: float)
signal finished_typing()
## The action to press to skip typing
@export var skip_action: String = "ui_cancel"
## The speed with which the text types out
@export var seconds_per_step: float = 0.02
## Automatically have a brief pause when these characters are encountered
@export var pause_at_characters: String = ".?!"
var dialogue_line:
set(next_dialogue_line):
dialogue_line = next_dialogue_line
custom_minimum_size = Vector2.ZERO
text = dialogue_line.text
get:
return dialogue_line
var last_wait_index: int = -1
var last_mutation_index: int = -1
var waiting_seconds: float = 0
var is_typing: bool = false:
set(value):
if is_typing != value and value == false:
finished_typing.emit()
is_typing = value
get:
return is_typing
func _process(delta: float) -> void:
if self.is_typing:
# Type out text
if visible_ratio < 1:
# See if we are waiting
if waiting_seconds > 0:
waiting_seconds = waiting_seconds - delta
# If we are no longer waiting then keep typing
if waiting_seconds <= 0:
type_next(delta, waiting_seconds)
else:
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()
# Start typing out the text
func type_out() -> void:
text = dialogue_line.text
visible_characters = 0
self.is_typing = true
waiting_seconds = 0
# Text isn't calculated until the next frame
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)
visible_characters = get_total_character_count()
self.is_typing = false
# Type out the next character(s)
func type_next(delta: float, seconds_needed: float) -> void:
if visible_characters == get_total_character_count():
return
if last_mutation_index != visible_characters:
last_mutation_index = visible_characters
mutate_inline_mutations(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
# Pause at literal [wait] directives
if last_wait_index != visible_characters and additional_waiting_seconds > 0:
last_wait_index = visible_characters
waiting_seconds += additional_waiting_seconds
paused_typing.emit(get_pause(visible_characters))
else:
visible_characters += 1
if visible_characters <= get_total_character_count():
spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, get_speed(visible_characters))
# See if there's time to type out some more in this frame
seconds_needed += seconds_per_step * (1.0 / get_speed(visible_characters))
if seconds_needed > delta:
waiting_seconds += seconds_needed
else:
type_next(delta, seconds_needed)
# Get the pause for the current typing position if there is one
func get_pause(at_index: int) -> float:
return dialogue_line.pauses.get(at_index, 0)
# Get the speed for the current typing position
func get_speed(at_index: int) -> float:
var speed: float = 1
for index in dialogue_line.speeds:
if index > at_index:
return speed
speed = dialogue_line.speeds[index]
return speed
# Run any mutations at the current typing position
func mutate_inline_mutations(index: int) -> void:
for inline_mutation in dialogue_line.inline_mutations:
# inline mutations are an array of arrays in the form of [character index, resolvable function]
if inline_mutation[0] > index:
return
if inline_mutation[0] == index:
# 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)

View File

@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"]
[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"]
[node name="DialogueLabel" type="RichTextLabel"]
anchors_preset = 10
anchor_right = 1.0
grow_horizontal = 2
mouse_filter = 1
bbcode_enabled = true
fit_content = true
scroll_active = false
shortcut_keys_enabled = false
meta_underlined = false
hint_underlined = false
deselect_on_focus_loss_enabled = false
visible_characters_behavior = 1
script = ExtResource("1_cital")

View File

@ -0,0 +1,43 @@
class_name DialogueLine extends RefCounted
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueResponse = preload("res://addons/dialogue_manager/dialogue_response.gd")
var type: String = DialogueConstants.TYPE_DIALOGUE
var next_id: String = ""
var character: String = ""
var character_replacements: Array[Dictionary] = []
var text: String = ""
var text_replacements: Array[Dictionary] = []
var translation_key: String = ""
var pauses: Dictionary = {}
var speeds: Dictionary = {}
var inline_mutations: Array[Array] = []
var responses: Array[DialogueResponse] = []
var extra_game_states: Array = []
var time = null
var mutation: Dictionary = {}
func _init(data: Dictionary = {}) -> void:
if data.size() > 0:
next_id = data.next_id
type = data.type
extra_game_states = data.extra_game_states
match type:
DialogueConstants.TYPE_DIALOGUE:
character = data.character
character_replacements = data.character_replacements
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
DialogueConstants.TYPE_MUTATION:
mutation = data.mutation

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
@icon("./assets/icon.svg")
class_name DialogueResource extends Resource
const DialogueManager = preload("res://addons/dialogue_manager/dialogue_manager.gd")
@export var titles: Dictionary = {}
@export var character_names: PackedStringArray = []
@export var first_title: String = ""
@export var lines: Dictionary = {}
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)
func get_titles() -> PackedStringArray:
return titles.keys()

View File

@ -0,0 +1,22 @@
class_name DialogueResponse extends RefCounted
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
var type: String = DialogueConstants.TYPE_RESPONSE
var next_id: String = ""
var is_allowed: bool = true
var text: String = ""
var text_replacements: Array[Dictionary] = []
var translation_key: String = ""
func _init(data: Dictionary = {}) -> void:
if data.size() > 0:
type = data.type
next_id = data.next_id
is_allowed = data.is_allowed
text = data.text
text_replacements = data.text_replacements
translation_key = data.translation_key

View File

@ -0,0 +1,40 @@
extends EditorTranslationParserPlugin
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> void:
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var text: String = file.get_as_text()
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
known_keys.append(character_name)
msgids_context_plural.append([character_name, "dialogue", ""])
# Add all dialogue lines and responses
var dialogue: Dictionary = data.lines
for key in dialogue.keys():
var line: Dictionary = dialogue.get(key)
if not line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: continue
if line.translation_key in known_keys: continue
known_keys.append(line.translation_key)
if line.translation_key == "" or line.translation_key == line.text:
msgids_context_plural.append([line.text, "", ""])
else:
msgids_context_plural.append([line.text, line.translation_key, ""])
func _get_recognized_extensions() -> PackedStringArray:
return ["dialogue"]

View File

@ -0,0 +1,212 @@
extends CanvasLayer
@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 dialogue resource
var resource: DialogueResource
## Temporary game states
var temporary_game_states: Array = []
## See if we are waiting for the player
var is_waiting_for_input: bool = false
## See if we are running a long mutation and should hide the balloon
var will_hide_balloon: bool = false
## The current line
var dialogue_line: DialogueLine:
set(next_dialogue_line):
is_waiting_for_input = false
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()
dialogue_line = next_dialogue_line
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
# 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)
# 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
# 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
func _ready() -> void:
response_template.hide()
balloon.hide()
balloon.custom_minimum_size.x = balloon.get_viewport_rect().size.x
Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated)
func _unhandled_input(_event: InputEvent) -> void:
# 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)
## 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)
### 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)
func _on_balloon_gui_input(event: InputEvent) -> void:
if not is_waiting_for_input: return
if dialogue_line.responses.size() > 0: return
# When there are no response options the balloon itself is the clickable thing
get_viewport().set_input_as_handled()
if event is InputEventMouseButton and event.is_pressed() and event.button_index == 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)
func _on_margin_resized() -> void:
handle_resize()

View File

@ -0,0 +1,73 @@
[gd_scene load_steps=5 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="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"]
[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_oj3c8"]
content_margin_left = 40.0
content_margin_top = 5.0
content_margin_right = 5.0
content_margin_bottom = 5.0
draw_center = false
[node name="ExampleBalloon" type="CanvasLayer"]
layer = 100
script = ExtResource("1_4u26j")
[node name="Balloon" type="ColorRect" parent="."]
color = Color(0, 0, 0, 1)
[node name="Margin" type="MarginContainer" parent="Balloon"]
layout_mode = 0
anchor_right = 1.0
offset_bottom = 119.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
[node name="VBox" type="VBoxContainer" parent="Balloon/Margin"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Margin/VBox"]
modulate = Color(1, 1, 1, 0.501961)
layout_mode = 2
mouse_filter = 1
bbcode_enabled = true
text = "Character"
fit_content = true
scroll_active = false
[node name="DialogueLabel" parent="Balloon/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"]
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
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
[connection signal="resized" from="Balloon/Margin" to="." method="_on_margin_resized"]

View File

@ -0,0 +1,87 @@
[gd_scene load_steps=8 format=3 uid="uid://b361p61jmf257"]
[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"]
[sub_resource type="Theme" id="Theme_isg48"]
default_font_size = 9
[sub_resource type="Theme" id="Theme_owda0"]
default_font_size = 9
[sub_resource type="Theme" id="Theme_fakos"]
default_font_size = 9
[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_oj3c8"]
content_margin_left = 20.0
content_margin_top = 2.0
content_margin_right = 2.0
content_margin_bottom = 2.0
draw_center = false
[node name="ExampleBalloon" type="CanvasLayer"]
layer = 100
script = ExtResource("1_4u26j")
[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
anchor_right = 1.0
offset_bottom = 75.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
[node name="VBox" type="VBoxContainer" parent="Balloon/Margin"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Margin/VBox"]
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"]
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
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
[connection signal="resized" from="Balloon/Margin" to="." method="_on_margin_resized"]

View File

@ -0,0 +1,109 @@
@tool
extends EditorImportPlugin
signal compiled_resource(resource: Resource)
const DialogueResource = preload("res://addons/dialogue_manager/dialogue_resource.gd")
const compiler_version = 8
var editor_plugin
func _get_importer_name() -> String:
# NOTE: A change to this forces a re-import of all dialogue
return "dialogue_manager_compiler_%s" % compiler_version
func _get_visible_name() -> String:
return "Dialogue"
func _get_import_order() -> int:
return -1000
func _get_priority() -> float:
return 1000.0
func _get_resource_type():
return "Resource"
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(["dialogue"])
func _get_save_extension():
return "tres"
func _get_preset_count() -> int:
return 0
func _get_preset_name(preset_index: int) -> String:
return "Unknown"
func _get_import_options(path: String, preset_index: int) -> Array:
# When the options array is empty there is a misleading error on export
# that actually means nothing so let's just have an invisible option.
return [{
name = "defaults",
default_value = true
}]
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
return false
func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error:
return compile_file(source_file, "%s.%s" % [save_path, _get_save_extension()])
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
var file: FileAccess = FileAccess.open(path, 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 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)
return err
# Get the current addon version
var config: ConfigFile = ConfigFile.new()
config.load("res://addons/dialogue_manager/plugin.cfg")
var version: String = config.get_value("plugin", "version")
# Save the results to a resource
var resource: DialogueResource = DialogueResource.new()
resource.set_meta("dialogue_manager_version", version)
resource.titles = data.titles
resource.first_title = data.first_title
resource.character_names = data.character_names
resource.lines = data.lines
if will_cascade_cache_data:
editor_plugin.add_to_dialogue_file_cache(path, resource_path, data)
err = ResourceSaver.save(resource, resource_path)
compiled_resource.emit(resource)
return err

Binary file not shown.

View File

@ -0,0 +1,400 @@
msgid ""
msgstr ""
"Project-Id-Version: Dialogue Manager\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.2.2\n"
msgid "start_a_new_file"
msgstr "Start a new file"
msgid "open_a_file"
msgstr "Open a file"
msgid "open.open"
msgstr "Open..."
msgid "open.no_recent_files"
msgstr "No recent files"
msgid "open.clear_recent_files"
msgstr "Clear recent files"
msgid "save_all_files"
msgstr "Save all files"
msgid "test_dialogue"
msgstr "Test dialogue"
msgid "search_for_text"
msgstr "Search for text"
msgid "insert"
msgstr "Insert"
msgid "translations"
msgstr "Translations"
msgid "settings"
msgstr "Settings"
msgid "docs"
msgstr "Docs"
msgid "insert.wave_bbcode"
msgstr "Wave BBCode"
msgid "insert.shake_bbcode"
msgstr "Shake BBCode"
msgid "insert.typing_pause"
msgstr "Typing pause"
msgid "insert.typing_speed_change"
msgstr "Typing speed change"
msgid "insert.auto_advance"
msgstr "Auto advance"
msgid "insert.templates"
msgstr "Templates"
msgid "insert.title"
msgstr "Title"
msgid "insert.dialogue"
msgstr "Dialogue"
msgid "insert.response"
msgstr "Response"
msgid "insert.random_lines"
msgstr "Random lines"
msgid "insert.random_text"
msgstr "Random text"
msgid "insert.actions"
msgstr "Actions"
msgid "insert.jump"
msgstr "Jump to title"
msgid "insert.end_dialogue"
msgstr "End dialogue"
msgid "generate_line_ids"
msgstr "Generate line IDs"
msgid "save_characters_to_csv"
msgstr "Save character names to CSV..."
msgid "save_to_csv"
msgstr "Save lines to CSV..."
msgid "import_from_csv"
msgstr "Import line changes from CSV..."
msgid "confirm_close"
msgstr "Save changes to '{path}'?"
msgid "confirm_close.save"
msgstr "Save changes"
msgid "confirm_close.discard"
msgstr "Discard"
msgid "buffer.save"
msgstr "Save"
msgid "buffer.save_as"
msgstr "Save as..."
msgid "buffer.close"
msgstr "Close"
msgid "buffer.close_all"
msgstr "Close all"
msgid "buffer.close_other_files"
msgstr "Close other files"
msgid "buffer.copy_file_path"
msgstr "Copy file path"
msgid "buffer.show_in_filesystem"
msgstr "Show in FileSystem"
msgid "settings.revert_to_default_test_scene"
msgstr "Revert to default test scene"
msgid "settings.autoload"
msgstr "Autload"
msgid "settings.path"
msgstr "Path"
msgid "settings.new_template"
msgstr "New dialogue files will start with template text"
msgid "settings.missing_keys"
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.wrap_long_lines"
msgstr "Wrap long lines"
msgid "settings.include_failed_responses"
msgstr "Include responses with failed conditions"
msgid "settings.custom_test_scene"
msgstr "Custom test scene (must extend BaseDialogueTestScene)"
msgid "settings.states_shortcuts"
msgstr "State Shortcuts"
msgid "settings.states_message"
msgstr "If an autoload is enabled here you can refer to its properties and methods without having to use its name."
msgid "settings.states_hint"
msgstr "ie. Instead of \"SomeState.some_property\" you could just use \"some_property\""
msgid "n_of_n"
msgstr "{index} of {total}"
msgid "search.previous"
msgstr "Previous"
msgid "search.next"
msgstr "Next"
msgid "search.match_case"
msgstr "Match case"
msgid "search.toggle_replace"
msgstr "Replace"
msgid "search.replace_with"
msgstr "Replace with:"
msgid "search.replace"
msgstr "Replace"
msgid "search.replace_all"
msgstr "Replace all"
msgid "files_list.filter"
msgstr "Filter files"
msgid "titles_list.filter"
msgstr "Filter titles"
msgid "errors.line_and_message"
msgstr "Error at {line}, {column}: {message}"
msgid "errors_in_script"
msgstr "You have errors in your script. Fix them and then try again."
msgid "errors_with_build"
msgstr "You need to fix dialogue errors before you can run your game."
msgid "errors.import_errors"
msgstr "There are errors in this imported file."
msgid "errors.already_imported"
msgstr "File already imported."
msgid "errors.duplicate_import"
msgstr "Duplicate import name."
msgid "errors.empty_title"
msgstr "Titles cannot be empty."
msgid "errors.duplicate_title"
msgstr "There is already a title with that name."
msgid "errors.nested_title"
msgstr "Titles cannot be nested."
msgid "errors.invalid_title_string"
msgstr "Titles can only contain alphanumeric characters and numbers."
msgid "errors.invalid_title_number"
msgstr "Titles cannot begin with a number."
msgid "errors.unknown_title"
msgstr "Unknown title."
msgid "errors.jump_to_invalid_title"
msgstr "This jump is pointing to an invalid title."
msgid "errors.title_has_no_content"
msgstr "That title has no content. Maybe change this to a \"=> END\"."
msgid "errors.invalid_expression"
msgstr "Expression is invalid."
msgid "errors.unexpected_condition"
msgstr "Unexpected condition."
msgid "errors.duplicate_id"
msgstr "This ID is already on another line."
msgid "errors.missing_id"
msgstr "This line is missing an ID."
msgid "errors.invalid_indentation"
msgstr "Invalid indentation."
msgid "errors.condition_has_no_content"
msgstr "A condition line needs an indented line below it."
msgid "errors.incomplete_expression"
msgstr "Incomplate expression."
msgid "errors.invalid_expression_for_value"
msgstr "Invalid expression for value."
msgid "errors.file_not_found"
msgstr "File not found."
msgid "errors.unexpected_end_of_expression"
msgstr "Unexpected end of expression."
msgid "errors.unexpected_function"
msgstr "Unexpected function."
msgid "errors.unexpected_bracket"
msgstr "Unexpected bracket."
msgid "errors.unexpected_closing_bracket"
msgstr "Unexpected closing bracket."
msgid "errors.missing_closing_bracket"
msgstr "Missing closing bracket."
msgid "errors.unexpected_operator"
msgstr "Unexpected operator."
msgid "errors.unexpected_comma"
msgstr "Unexpected comma."
msgid "errors.unexpected_colon"
msgstr "Unexpected colon."
msgid "errors.unexpected_dot"
msgstr "Unexpected dot."
msgid "errors.unexpected_boolean"
msgstr "Unexpected boolean."
msgid "errors.unexpected_string"
msgstr "Unexpected string."
msgid "errors.unexpected_number"
msgstr "Unexpected number."
msgid "errors.unexpected_variable"
msgstr "Unexpected variable."
msgid "errors.invalid_index"
msgstr "Invalid index."
msgid "errors.unexpected_assignment"
msgstr "Unexpected assignment."
msgid "errors.unknown"
msgstr "Unknown syntax."
msgid "update.available"
msgstr "v{version} available"
msgid "update.is_available_for_download"
msgstr "Version %s is available for download!"
msgid "update.downloading"
msgstr "Downloading..."
msgid "update.download_update"
msgstr "Download update"
msgid "update.needs_reload"
msgstr "The project needs to be reloaded to install the update."
msgid "update.reload_ok_button"
msgstr "Reload project"
msgid "update.reload_cancel_button"
msgstr "Do it later"
msgid "update.reload_project"
msgstr "Reload project"
msgid "update.release_notes"
msgstr "Read release notes"
msgid "update.success"
msgstr "Dialogue Manager is now v{version}."
msgid "update.failed"
msgstr "There was a problem downloading the update."
msgid "runtime.no_resource"
msgstr "No dialogue resource provided."
msgid "runtime.no_content"
msgstr "\"{file_path}\" has no content."
msgid "runtime.errors"
msgstr "You have {count} errors in your dialogue text."
msgid "runtime.error_detail"
msgstr "Line {line}: {message}"
msgid "runtime.errors_see_details"
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}\"."
msgid "runtime.left_hand_size_cannot_be_assigned_to"
msgstr "Left hand side of expression cannot be assigned to."
msgid "runtime.key_not_found"
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.method_not_found"
msgstr "\"{method}\" is not a method on any game states ({states})"
msgid "runtime.signal_not_found"
msgstr "\"{signal_name}\" is not a signal on any game states ({states})"
msgid "runtime.method_not_callable"
msgstr "\"{method}\" is not a callable method on \"{object}\""
msgid "runtime.unknown_operator"
msgstr "Unknown operator."
msgid "runtime.something_went_wrong"
msgstr "Something went wrong."

View File

@ -0,0 +1,390 @@
msgid ""
msgstr ""
"Project-Id-Version: Dialogue Manager\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8-bit\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 "docs"
msgstr ""
msgid "insert.wave_bbcode"
msgstr ""
msgid "insert.shake_bbcode"
msgstr ""
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 ""
msgid "save_to_csv"
msgstr ""
msgid "import_from_csv"
msgstr ""
msgid "confirm_close"
msgstr ""
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 ""
msgid "settings.revert_to_default_test_scene"
msgstr ""
msgid "settings.autoload"
msgstr ""
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.include_failed_responses"
msgstr ""
msgid "settings.custom_test_scene"
msgstr ""
msgid "settings.states_shortcuts"
msgstr ""
msgid "settings.states_message"
msgstr ""
msgid "settings.states_hint"
msgstr ""
msgid "n_of_n"
msgstr ""
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.line_and_message"
msgstr ""
msgid "errors_in_script"
msgstr ""
msgid "errors_with_build"
msgstr ""
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 ""
msgid "errors.invalid_expression"
msgstr ""
msgid "errors.unexpected_condition"
msgstr ""
msgid "errors.duplicate_id"
msgstr ""
msgid "errors.missing_id"
msgstr ""
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 ""
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 ""
msgid "update.is_available_for_download"
msgstr ""
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 ""
msgid "update.failed"
msgstr ""
msgid "runtime.no_resource"
msgstr ""
msgid "runtime.no_content"
msgstr ""
msgid "runtime.errors"
msgstr ""
msgid "runtime.error_detail"
msgstr ""
msgid "runtime.errors_see_details"
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 ""
msgid "runtime.left_hand_size_cannot_be_assigned_to"
msgstr ""
msgid "runtime.key_not_found"
msgstr ""
msgid "runtime.property_not_found"
msgstr ""
msgid "runtime.method_not_found"
msgstr ""
msgid "runtime.signal_not_found"
msgstr ""
msgid "runtime.method_not_callable"
msgstr ""
msgid "runtime.unknown_operator"
msgstr ""
msgid "runtime.something_went_wrong"
msgstr ""

View File

@ -0,0 +1,7 @@
[plugin]
name="Dialogue Manager"
description="A simple but powerful branching dialogue system"
author="Nathan Hoad"
version="2.16.2"
script="plugin.gd"

View File

@ -0,0 +1,302 @@
@tool
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")
var import_plugin: DialogueImportPlugin
var translation_parser_plugin: DialogueTranslationParserPlugin
var main_view
var dialogue_file_cache: Dictionary = {}
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())
if Engine.is_editor_hint():
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)
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)
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)
get_editor_interface().get_file_system_dock().files_moved.disconnect(_on_files_moved)
remove_tool_menu_item("Create copy of dialogue example balloon...")
func _has_main_screen() -> bool:
return true
func _make_visible(next_visible: bool) -> void:
if is_instance_valid(main_view):
main_view.visible = next_visible
func _get_plugin_name() -> String:
return "Dialogue"
func _get_plugin_icon() -> Texture2D:
return load("res://addons/dialogue_manager/assets/icon.svg")
func _handles(object) -> bool:
return object is DialogueResource
func _edit(object) -> void:
if is_instance_valid(main_view) and is_instance_valid(object):
main_view.open_resource(object)
func _apply_changes() -> void:
if is_instance_valid(main_view):
main_view.apply_changes()
func _build() -> bool:
# 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
## 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)
## 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)
## 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)
# 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)
# Update any other files that import the moved file
var dependents = dialogue_file_cache.values().filter(func(d): return from_path in d.dependencies)
for dependent in dependents:
dependent.dependencies.erase(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)
main_view.pristine_text = main_view.code_edit.text
# 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 = FileAccess.open(dependent.path, FileAccess.WRITE)
file.store_string(text)
save_dialogue_cache()
## 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)
## 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
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:
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
# 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
func _copy_dialogue_balloon() -> void:
var scale: float = get_editor_interface().get_editor_scale()
var directory_dialog: FileDialog = FileDialog.new()
var label: Label = Label.new()
label.text = "Dialogue balloon files will be copied into chosen directory."
directory_dialog.get_vbox().add_child(label)
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)
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)
file_contents = file.get_as_text()
file = FileAccess.open(path + "/balloon.gd", 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")
directory_dialog.queue_free()
)
get_editor_interface().get_base_control().add_child(directory_dialog)
directory_dialog.popup_centered()
### 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)
if is_instance_valid(main_view):
main_view.close_file(file)

View File

@ -0,0 +1,29 @@
class_name BaseDialogueTestScene extends Node2D
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
@onready var title: String = DialogueSettings.get_user_value("run_title")
@onready var resource: DialogueResource = load(DialogueSettings.get_user_value("run_resource_path"))
func _ready():
var screen_index: int = DisplayServer.get_primary_screen()
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)
func _enter_tree() -> void:
DialogueSettings.set_user_value("is_running_test_scene", false)
### Signals
func _on_dialogue_ended(_resource: DialogueResource):
get_tree().quit()

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://addons/dialogue_manager/test_scene.gd" id="1_yupoh"]
[node name="TestScene" type="Node2D"]
script = ExtResource("1_yupoh")

View File

@ -0,0 +1,986 @@
@tool
extends Control
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
const OPEN_OPEN = 100
const OPEN_CLEAR = 101
const TRANSLATIONS_GENERATE_LINE_IDS = 100
const TRANSLATIONS_SAVE_CHARACTERS_TO_CSV = 201
const TRANSLATIONS_SAVE_TO_CSV = 202
const TRANSLATIONS_IMPORT_FROM_CSV = 203
const ITEM_SAVE = 100
const ITEM_SAVE_AS = 101
const ITEM_CLOSE = 102
const ITEM_CLOSE_ALL = 103
const ITEM_CLOSE_OTHERS = 104
const ITEM_COPY_PATH = 200
const ITEM_SHOW_IN_FILESYSTEM = 201
enum TranslationSource {
CharacterNames,
Lines
}
@onready var parse_timer := $ParseTimer
# Dialogs
@onready var new_dialog: FileDialog = $NewDialog
@onready var save_dialog: FileDialog = $SaveDialog
@onready var open_dialog: FileDialog = $OpenDialog
@onready var export_dialog: FileDialog = $ExportDialog
@onready var import_dialog: FileDialog = $ImportDialog
@onready var errors_dialog: AcceptDialog = $ErrorsDialog
@onready var settings_dialog: AcceptDialog = $SettingsDialog
@onready var settings_view := $SettingsDialog/SettingsView
@onready var build_error_dialog: AcceptDialog = $BuildErrorDialog
@onready var close_confirmation_dialog: ConfirmationDialog = $CloseConfirmationDialog
@onready var updated_dialog: AcceptDialog = $UpdatedDialog
# Toolbar
@onready var new_button: Button = %NewButton
@onready var open_button: MenuButton = %OpenButton
@onready var save_all_button: Button = %SaveAllButton
@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 docs_button: Button = %DocsButton
@onready var version_label: Label = %VersionLabel
@onready var update_button: Button = %UpdateButton
@onready var search_and_replace := %SearchAndReplace
# Code editor
@onready var content: HSplitContainer = %Content
@onready var files_list := %FilesList
@onready var files_popup_menu: PopupMenu = %FilesPopupMenu
@onready var title_list := %TitleList
@onready var code_edit := %CodeEdit
@onready var errors_panel := %ErrorsPanel
# The Dialogue Manager plugin
var editor_plugin: EditorPlugin
# The currently open file
var current_file_path: String = "":
set(next_current_file_path):
current_file_path = next_current_file_path
files_list.current_file_path = current_file_path
if current_file_path == "":
save_all_button.disabled = true
test_button.disabled = true
search_button.disabled = true
insert_button.disabled = true
translations_button.disabled = true
content.dragger_visibility = SplitContainer.DRAGGER_HIDDEN
files_list.hide()
title_list.hide()
code_edit.hide()
else:
test_button.disabled = false
search_button.disabled = false
insert_button.disabled = false
translations_button.disabled = false
content.dragger_visibility = SplitContainer.DRAGGER_VISIBLE
files_list.show()
title_list.show()
code_edit.show()
code_edit.text = open_buffers[current_file_path].text
code_edit.errors = []
code_edit.clear_undo_history()
code_edit.set_cursor(DialogueSettings.get_caret(current_file_path))
code_edit.grab_focus()
_on_code_edit_text_changed()
errors_panel.errors = []
code_edit.errors = []
get:
return current_file_path
# A reference to the currently open files and their last saved text
var open_buffers: Dictionary = {}
# Which thing are we exporting translations for?
var translation_source: TranslationSource = TranslationSource.Lines
func _ready() -> void:
apply_theme()
# Start with nothing open
self.current_file_path = ""
# Set up the update checker
version_label.text = "v%s" % update_button.get_version()
update_button.editor_plugin = editor_plugin
update_button.on_before_refresh = func on_before_refresh():
# Save everything
DialogueSettings.set_user_value("just_refreshed", {
current_file_path = current_file_path,
open_buffers = open_buffers
})
return true
# Did we just load from an addon version refresh?
var just_refreshed = DialogueSettings.get_user_value("just_refreshed", null)
if just_refreshed != null:
DialogueSettings.set_user_value("just_refreshed", null)
call_deferred("load_from_version_refresh", just_refreshed)
# Hook up the search toolbar
search_and_replace.code_edit = code_edit
# Connect menu buttons
insert_button.get_popup().id_pressed.connect(_on_insert_button_menu_id_pressed)
translations_button.get_popup().id_pressed.connect(_on_translations_button_menu_id_pressed)
code_edit.main_view = self
code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DialogueSettings.get_setting("wrap_lines", false) else TextEdit.LINE_WRAPPING_NONE
var editor_settings: EditorSettings = editor_plugin.get_editor_interface().get_editor_settings()
editor_settings.settings_changed.connect(_on_editor_settings_changed)
_on_editor_settings_changed()
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")
settings_view.editor_plugin = editor_plugin
errors_dialog.dialog_text = DialogueConstants.translate("errors_in_script")
func _unhandled_input(event: InputEvent) -> void:
if not visible: return
if event is InputEventKey and event.is_pressed():
match event.as_text():
"Ctrl+Alt+S":
save_file(current_file_path)
"Ctrl+W":
get_viewport().set_input_as_handled()
close_file(current_file_path)
"Ctrl+F5":
_on_test_button_pressed()
func apply_changes() -> void:
save_files()
# Load back to the previous buffer regardless of if it was actually saved
func load_from_version_refresh(just_refreshed: Dictionary) -> void:
if just_refreshed.has("current_file_content"):
# We just loaded from a version before multiple buffers
var file: FileAccess = FileAccess.open(just_refreshed.current_file_path, FileAccess.READ)
var file_text: String = file.get_as_text()
open_buffers[just_refreshed.current_file_path] = {
pristine_text = file_text,
text = just_refreshed.current_file_content
}
else:
open_buffers = just_refreshed.open_buffers
if just_refreshed.current_file_path != "":
editor_plugin.get_editor_interface().edit_resource(load(just_refreshed.current_file_path))
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.popup_centered()
func new_file(path: String, content: String = "") -> void:
if open_buffers.has(path):
remove_file_from_open_buffers(path)
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
if content == "":
if DialogueSettings.get_setting("new_with_template", true):
file.store_string("\n".join([
"~ this_is_a_node_title",
"",
"Nathan: [[Hi|Hello|Howdy]], this is some dialogue.",
"Nathan: Here are some choices.",
"- First one",
"\tNathan: You picked the first one.",
"- Second one",
"\tNathan: You picked the second one.",
"- Start again => this_is_a_node_title",
"- End the conversation => END",
"Nathan: For more information see the online documentation.",
"",
"=> END"
]))
else:
file.store_string(content)
editor_plugin.get_editor_interface().get_resource_filesystem().scan()
# Open a dialogue resource for editing
func open_resource(resource: DialogueResource) -> void:
open_file(resource.resource_path)
func open_file(path: String) -> void:
if not open_buffers.has(path):
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var text = file.get_as_text()
open_buffers[path] = {
cursor = Vector2.ZERO,
text = text,
pristine_text = text
}
DialogueSettings.add_recent_file(path)
build_open_menu()
files_list.files = open_buffers.keys()
files_list.select_file(path)
self.current_file_path = path
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)
# Save any open files
func save_files() -> void:
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)
# 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
# Save a file
func save_file(path: String) -> void:
var buffer = open_buffers[path]
files_list.mark_file_as_unsaved(path, false)
save_all_button.disabled = files_list.unsaved_files.size() == 0
# Don't bother saving if there is nothing to save
if buffer.text == buffer.pristine_text:
return
buffer.pristine_text = buffer.text
# Save the current text
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
file.store_string(buffer.text)
file.close()
func close_file(file: String) -> void:
if not file in open_buffers.keys(): return
var buffer = open_buffers[file]
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.popup_centered()
func remove_file_from_open_buffers(file: String) -> void:
if not file in open_buffers.keys(): return
var current_index = open_buffers.keys().find(file)
open_buffers.erase(file)
if open_buffers.size() == 0:
self.current_file_path = ""
else:
current_index = clamp(current_index, 0, open_buffers.size() - 1)
self.current_file_path = open_buffers.keys()[current_index]
files_list.files = open_buffers.keys()
# Apply theme colors and icons to the UI
func apply_theme() -> void:
if is_instance_valid(editor_plugin) and is_instance_valid(code_edit):
var scale: float = editor_plugin.get_editor_interface().get_editor_scale()
var editor_settings = editor_plugin.get_editor_interface().get_editor_settings()
code_edit.theme_overrides = {
scale = scale,
background_color = editor_settings.get_setting("text_editor/theme/highlighting/background_color"),
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"),
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"),
mutations_color = editor_settings.get_setting("text_editor/theme/highlighting/function_color"),
members_color = editor_settings.get_setting("text_editor/theme/highlighting/member_variable_color"),
strings_color = editor_settings.get_setting("text_editor/theme/highlighting/string_color"),
numbers_color = editor_settings.get_setting("text_editor/theme/highlighting/number_color"),
symbols_color = editor_settings.get_setting("text_editor/theme/highlighting/symbol_color"),
comments_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_color"),
jumps_color = Color(editor_settings.get_setting("text_editor/theme/highlighting/control_flow_keyword_color"), 0.7),
font_size = editor_settings.get_setting("interface/editor/code_font_size")
}
new_button.icon = get_theme_icon("New", "EditorIcons")
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")
save_all_button.icon = get_theme_icon("Save", "EditorIcons")
save_all_button.tooltip_text = DialogueConstants.translate("start_all_files")
test_button.icon = get_theme_icon("PlayScene", "EditorIcons")
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")
insert_button.icon = get_theme_icon("RichTextEffect", "EditorIcons")
insert_button.text = DialogueConstants.translate("insert")
translations_button.icon = get_theme_icon("Translation", "EditorIcons")
translations_button.text = DialogueConstants.translate("translations")
settings_button.icon = get_theme_icon("Tools", "EditorIcons")
settings_button.tooltip_text = DialogueConstants.translate("settings")
docs_button.icon = get_theme_icon("Help", "EditorIcons")
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_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)
# 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_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)
# 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
### Helpers
# Refresh the open menu with the latest files
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_separator()
var recent_files = DialogueSettings.get_recent_files()
if recent_files.size() == 0:
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)
menu.add_separator()
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)
# Get the last place a CSV, etc was exported
func get_last_export_path(extension: String) -> String:
var filename = current_file_path.get_file().replace(".dialogue", "." + extension)
return DialogueSettings.get_user_value("last_export_path", current_file_path.get_base_dir()) + "/" + filename
# Check the current text for errors
func parse() -> void:
# Skip if nothing to parse
if current_file_path == "": return
var parser = DialogueManagerParser.new()
var errors: Array[Dictionary] = []
if parser.parse(code_edit.text, current_file_path) != OK:
errors = parser.get_errors()
code_edit.errors = errors
errors_panel.errors = errors
parser.free()
func show_build_error_dialog() -> void:
build_error_dialog.dialog_text = DialogueConstants.translate("errors_with_build")
build_error_dialog.popup_centered()
# Generate translation line IDs for any line that doesn't already have one
func generate_translations_keys() -> void:
randomize()
seed(Time.get_unix_time_from_system())
var parser = DialogueManagerParser.new()
var cursor: Vector2 = code_edit.get_cursor()
var lines: PackedStringArray = code_edit.text.split("\n")
var key_regex = RegEx.new()
key_regex.compile("\\[ID:(?<key>.*?)\\]")
# Make list of known keys
var known_keys = {}
for i in range(0, lines.size()):
var line = lines[i]
var found = key_regex.search(line)
if found:
var text = ""
var l = line.replace(found.strings[0], "").strip_edges().strip_edges()
if l.begins_with("- "):
text = parser.extract_response_prompt(l)
elif ":" in l:
text = l.split(":")[1]
else:
text = l
known_keys[found.strings[found.names.get("key")]] = text
# Add in any that are missing
for i in lines.size():
var line = lines[i]
var l = line.strip_edges()
if parser.is_line_empty(l): continue
if parser.is_condition_line(l, true): continue
if parser.is_title_line(l): continue
if parser.is_mutation_line(l): continue
if parser.is_goto_line(l): continue
if parser.is_import_line(l): continue
if "[ID:" in line: continue
var key = "t" + str(randi() % 1000000).sha1_text().substr(0, 10)
while key in known_keys:
key = "t" + str(randi() % 1000000).sha1_text().substr(0, 10)
var text = ""
if l.begins_with("- "):
text = parser.extract_response_prompt(l)
else:
text = l.substr(l.find(":") + 1)
lines[i] = line.replace(text, text + " [ID:%s]" % key)
known_keys[key] = text
code_edit.text = "\n".join(lines)
code_edit.set_cursor(cursor)
_on_code_edit_text_changed()
parser.free()
# Add a translation file to the project settings
func add_path_to_project_translations(path: String) -> void:
var translations: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations")
if not path in translations:
translations.append(path)
ProjectSettings.save()
# Export dialogue and responses to CSV
func export_translations_to_csv(path: String) -> void:
var file: FileAccess
# If the file exists, open it first and work out which keys are already in it
var existing_csv = {}
var commas = []
if FileAccess.file_exists(path):
file = FileAccess.open(path, FileAccess.READ)
var is_first_line = true
var line: Array
while !file.eof_reached():
line = file.get_csv_line()
if is_first_line:
is_first_line = false
for i in range(2, line.size()):
commas.append("")
# Make sure the line isn't empty before adding it
if line.size() > 0 and line[0].strip_edges() != "":
existing_csv[line[0]] = line
# Start a new file
file = FileAccess.open(path, FileAccess.WRITE)
if not file.file_exists(path):
file.store_csv_line(["keys", "en"])
# Write our translations to file
var known_keys: PackedStringArray = []
var dialogue: Dictionary = DialogueManagerParser.parse_string(code_edit.text, current_file_path).lines
# Make a list of stuff that needs to go into the file
var lines_to_save = []
for key in dialogue.keys():
var line: Dictionary = dialogue.get(key)
if not line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: continue
if line.translation_key in known_keys: continue
known_keys.append(line.translation_key)
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)
existing_csv.erase(line.translation_key)
else:
lines_to_save.append(PackedStringArray([line.translation_key, line.text] + commas))
# Store lines in the file, starting with anything that already exists that hasn't been touched
for line in existing_csv.values():
file.store_csv_line(line)
for line in lines_to_save:
file.store_csv_line(line)
file.close()
editor_plugin.get_editor_interface().get_resource_filesystem().scan()
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")
call_deferred("add_path_to_project_translations", translation_path)
func export_character_names_to_csv(path: String) -> void:
var file: FileAccess
# If the file exists, open it first and work out which keys are already in it
var existing_csv = {}
var commas = []
if FileAccess.file_exists(path):
file = FileAccess.open(path, FileAccess.READ)
var is_first_line = true
var line: Array
while !file.eof_reached():
line = file.get_csv_line()
if is_first_line:
is_first_line = false
for i in range(2, line.size()):
commas.append("")
# Make sure the line isn't empty before adding it
if line.size() > 0 and line[0].strip_edges() != "":
existing_csv[line[0]] = line
# Start a new file
file = FileAccess.open(path, FileAccess.WRITE)
if not file.file_exists(path):
file.store_csv_line(["keys", "en"])
# Write our translations to file
var known_keys: PackedStringArray = []
var character_names: PackedStringArray = DialogueManagerParser.parse_string(code_edit.text, current_file_path).character_names
# Make a list of stuff that needs to go into the file
var lines_to_save = []
for character_name in character_names:
if character_name in known_keys: continue
known_keys.append(character_name)
if existing_csv.has(character_name):
var existing_line = existing_csv.get(character_name)
existing_line[1] = character_name
lines_to_save.append(existing_line)
existing_csv.erase(character_name)
else:
lines_to_save.append(PackedStringArray([character_name, character_name] + commas))
# Store lines in the file, starting with anything that already exists that hasn't been touched
for line in existing_csv.values():
file.store_csv_line(line)
for line in lines_to_save:
file.store_csv_line(line)
file.close()
editor_plugin.get_editor_interface().get_resource_filesystem().scan()
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")
call_deferred("add_path_to_project_translations", translation_path)
# Import changes back from an exported CSV by matching translation keys
func import_translations_from_csv(path: String) -> void:
var cursor: Vector2 = code_edit.get_cursor()
if not FileAccess.file_exists(path): return
# Open the CSV file and build a dictionary of the known keys
var keys: Dictionary = {}
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var csv_line: Array
while !file.eof_reached():
csv_line = file.get_csv_line()
if csv_line.size() > 1:
keys[csv_line[0]] = csv_line[1]
var parser: DialogueManagerParser = DialogueManagerParser.new()
# Now look over each line in the dialogue and replace the content for matched keys
var lines: PackedStringArray = code_edit.text.split("\n")
var start_index: int = 0
var end_index: int = 0
for i in range(0, lines.size()):
var line: String = lines[i]
var translation_key: String = parser.extract_translation(line)
if keys.has(translation_key):
if parser.is_dialogue_line(line):
start_index = 0
# See if we need to skip over a character name
line = line.replace("\\:", "!ESCAPED_COLON!")
if ": " in line:
start_index = line.find(": ") + 2
lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]").replace("!ESCAPED_COLON!", ":")
elif parser.is_response_line(line):
start_index = line.find("- ") + 2
# See if we need to skip over a character name
line = line.replace("\\:", "!ESCAPED_COLON!")
if ": " in line:
start_index = line.find(": ") + 2
end_index = line.length()
if " =>" in line:
end_index = line.find(" =>")
if " [if " in line:
end_index = line.find(" [if ")
lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]" + line.substr(end_index)).replace("!ESCAPED_COLON!", ":")
code_edit.text = "\n".join(lines)
code_edit.set_cursor(cursor)
parser.free()
### Signals
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")
func _on_open_menu_id_pressed(id: int) -> void:
match id:
OPEN_OPEN:
open_dialog.popup_centered()
OPEN_CLEAR:
DialogueSettings.clear_recent_files()
build_open_menu()
_:
var menu = open_button.get_popup()
var item = menu.get_item_text(menu.get_item_index(id))
open_file(item)
func _on_files_list_file_selected(file_path: String) -> void:
self.current_file_path = file_path
func _on_insert_button_menu_id_pressed(id: int) -> void:
match id:
0:
code_edit.insert_bbcode("[wave amp=25 freq=5]", "[/wave]")
1:
code_edit.insert_bbcode("[shake rate=20 level=10]", "[/shake]")
3:
code_edit.insert_bbcode("[wait=1]")
4:
code_edit.insert_bbcode("[speed=0.2]")
5:
code_edit.insert_bbcode("[next=auto]")
6:
code_edit.insert_text("~ title")
7:
code_edit.insert_text("Nathan: This is Some Dialogue")
8:
code_edit.insert_text("Nathan: Choose a Response...\n- Option 1\n\tNathan: You chose option 1\n- Option 2\n\tNathan: You chose option 2")
9:
code_edit.insert_text("% Nathan: This is random line 1.\n% Nathan: This is random line 2.\n%1 Nathan: This is weighted random line 3.")
10:
code_edit.insert_text("Nathan: [[Hi|Hello|Howdy]]")
11:
code_edit.insert_text("=> title")
12:
code_edit.insert_text("=> END")
func _on_translations_button_menu_id_pressed(id: int) -> void:
match id:
TRANSLATIONS_GENERATE_LINE_IDS:
generate_translations_keys()
TRANSLATIONS_SAVE_CHARACTERS_TO_CSV:
translation_source = TranslationSource.CharacterNames
export_dialog.filters = PackedStringArray(["*.csv ; Translation CSV"])
export_dialog.current_path = get_last_export_path("csv")
export_dialog.popup_centered()
TRANSLATIONS_SAVE_TO_CSV:
translation_source = TranslationSource.Lines
export_dialog.filters = PackedStringArray(["*.csv ; Translation CSV"])
export_dialog.current_path = get_last_export_path("csv")
export_dialog.popup_centered()
TRANSLATIONS_IMPORT_FROM_CSV:
import_dialog.current_path = get_last_export_path("csv")
import_dialog.popup_centered()
func _on_export_dialog_file_selected(path: String) -> void:
DialogueSettings.set_user_value("last_export_path", path.get_base_dir())
match path.get_extension():
"csv":
match translation_source:
TranslationSource.CharacterNames:
export_character_names_to_csv(path)
TranslationSource.Lines:
export_translations_to_csv(path)
func _on_import_dialog_file_selected(path: String) -> void:
DialogueSettings.set_user_value("last_export_path", path.get_base_dir())
import_translations_from_csv(path)
func _on_main_view_theme_changed():
apply_theme()
func _on_main_view_visibility_changed() -> void:
if visible and is_instance_valid(code_edit):
code_edit.grab_focus()
func _on_new_button_pressed() -> void:
new_dialog.current_file = ""
new_dialog.popup_centered()
func _on_new_dialog_file_selected(path: String) -> void:
new_file(path)
open_file(path)
func _on_save_dialog_file_selected(path: String) -> void:
new_file(path, code_edit.text)
open_file(path)
func _on_open_button_about_to_popup() -> void:
build_open_menu()
func _on_open_dialog_file_selected(path: String) -> void:
open_file(path)
func _on_save_all_button_pressed() -> void:
save_files()
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
parse_timer.start(1)
func _on_code_edit_active_title_change(title: String) -> void:
title_list.select_title(title)
DialogueSettings.set_user_value("run_title", title)
func _on_code_edit_caret_changed() -> void:
DialogueSettings.set_caret(current_file_path, code_edit.get_cursor())
func _on_code_edit_error_clicked(line_number: int) -> void:
errors_panel.show_error_for_line_number(line_number)
func _on_title_list_title_selected(title: String) -> void:
code_edit.go_to_title(title)
code_edit.grab_focus()
func _on_parse_timer_timeout() -> void:
parse_timer.stop()
parse()
func _on_errors_panel_error_pressed(line_number: int, column_number: int) -> void:
code_edit.set_caret_line(line_number)
code_edit.set_caret_column(column_number)
code_edit.grab_focus()
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
func _on_search_and_replace_open_requested() -> void:
search_button.set_pressed_no_signal(true)
search_and_replace.visible = true
func _on_search_and_replace_close_requested() -> void:
search_button.set_pressed_no_signal(false)
search_and_replace.visible = false
code_edit.grab_focus()
func _on_settings_button_pressed() -> void:
settings_dialog.popup_centered()
func _on_settings_view_script_button_pressed(path: String) -> void:
settings_dialog.hide()
editor_plugin.get_editor_interface().edit_resource(load(path))
func _on_test_button_pressed() -> void:
apply_changes()
if errors_panel.errors.size() > 0:
errors_dialog.popup_centered()
return
DialogueSettings.set_user_value("is_running_test_scene", true)
DialogueSettings.set_user_value("run_resource_path", current_file_path)
var test_scene_path: String = DialogueSettings.get_setting("custom_test_scene_path", "res://addons/dialogue_manager/test_scene.tscn")
editor_plugin.get_editor_interface().play_custom_scene(test_scene_path)
func _on_settings_dialog_confirmed() -> void:
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_docs_button_pressed() -> void:
OS.shell_open("https://github.com/nathanhoad/godot_dialogue_manager")
func _on_files_list_file_popup_menu_requested(at_position: Vector2) -> void:
files_popup_menu.position = Vector2(get_viewport().position) + files_list.global_position + at_position
files_popup_menu.popup()
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_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)
func _on_files_popup_menu_id_pressed(id: int) -> void:
match id:
ITEM_SAVE:
save_file(current_file_path)
ITEM_SAVE_AS:
save_dialog.popup_centered()
ITEM_CLOSE:
close_file(current_file_path)
ITEM_CLOSE_ALL:
for path in open_buffers.keys():
close_file(path)
ITEM_CLOSE_OTHERS:
for path in open_buffers.keys():
if path != current_file_path:
close_file(path)
ITEM_COPY_PATH:
DisplayServer.clipboard_set(current_file_path)
ITEM_SHOW_IN_FILESYSTEM:
show_file_in_filesystem(current_file_path)
func _on_code_edit_external_file_requested(path: String, title: String) -> void:
open_file(path)
if title != "":
code_edit.go_to_title(title)
else:
code_edit.set_caret_line(0)
func _on_close_confirmation_dialog_confirmed() -> void:
save_file(current_file_path)
remove_file_from_open_buffers(current_file_path)
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()

View File

@ -0,0 +1,348 @@
[gd_scene load_steps=13 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"]
[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="2_npj2k"]
[ext_resource type="PackedScene" uid="uid://ctns6ouwwd68i" path="res://addons/dialogue_manager/components/title_list.tscn" id="2_onb4i"]
[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="PackedScene" uid="uid://cpg4lg1r3ff6m" path="res://addons/dialogue_manager/views/settings_view.tscn" id="9_8bf36"]
[sub_resource type="Image" id="Image_0jq4a"]
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_fguub"]
image = SubResource("Image_0jq4a")
[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")
[node name="MainView" 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_h6qfq")
[node name="ParseTimer" type="Timer" parent="."]
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
theme_override_constants/margin_left = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
metadata/_edit_layout_mode = 1
[node name="Content" type="HSplitContainer" parent="Margin"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
dragger_visibility = 1
[node name="SidePanel" type="VBoxContainer" parent="Margin/Content"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
size_flags_horizontal = 3
[node name="Toolbar" type="HBoxContainer" parent="Margin/Content/SidePanel"]
layout_mode = 2
[node name="NewButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"]
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
layout_mode = 2
disabled = true
flat = true
[node name="Bookmarks" type="VSplitContainer" parent="Margin/Content/SidePanel"]
layout_mode = 2
size_flags_vertical = 3
[node name="FilesList" parent="Margin/Content/SidePanel/Bookmarks" instance=ExtResource("2_npj2k")]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="FilesPopupMenu" type="PopupMenu" parent="Margin/Content/SidePanel/Bookmarks/FilesList"]
unique_name_in_owner = true
[node name="TitleList" parent="Margin/Content/SidePanel/Bookmarks" instance=ExtResource("2_onb4i")]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="CodePanel" type="VBoxContainer" parent="Margin/Content"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 4.0
[node name="Toolbar" type="HBoxContainer" parent="Margin/Content/CodePanel"]
layout_mode = 2
[node name="InsertButton" type="MenuButton" parent="Margin/Content/CodePanel/Toolbar"]
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")
popup/item_0/id = 0
popup/item_1/text = ""
popup/item_1/id = -1
popup/item_1/separator = true
popup/item_2/text = "Save to CSV..."
popup/item_2/icon = SubResource("ImageTexture_fguub")
popup/item_2/id = 2
popup/item_3/text = "Import changes from CSV..."
popup/item_3/icon = SubResource("ImageTexture_fguub")
popup/item_3/id = 3
popup/item_4/text = ""
popup/item_4/id = -1
popup/item_4/separator = true
popup/item_5/text = "Save to PO..."
popup/item_5/icon = SubResource("ImageTexture_fguub")
popup/item_5/id = 5
[node name="Separator" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"]
layout_mode = 2
[node name="SearchButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"]
unique_name_in_owner = true
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"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Test dialogue"
disabled = true
icon = SubResource("ImageTexture_fguub")
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"]
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"]
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"
vertical_alignment = 1
[node name="UpdateButton" parent="Margin/Content/CodePanel/Toolbar" instance=ExtResource("2_ph3vs")]
unique_name_in_owner = true
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")]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_colors/bookmark_color = Color(1, 0.333333, 0.333333, 1)
text = ""
[node name="ErrorsPanel" parent="Margin/Content/CodePanel" instance=ExtResource("7_5cvl4")]
unique_name_in_owner = true
layout_mode = 2
[node name="NewDialog" type="FileDialog" parent="."]
size = Vector2i(600, 500)
min_size = Vector2i(600, 500)
dialog_hide_on_ok = true
filters = PackedStringArray("*.dialogue ; Dialogue")
[node name="SaveDialog" type="FileDialog" parent="."]
size = Vector2i(600, 500)
min_size = Vector2i(600, 500)
dialog_hide_on_ok = true
filters = PackedStringArray("*.dialogue ; Dialogue")
[node name="OpenDialog" type="FileDialog" parent="."]
title = "Open a File"
size = Vector2i(600, 500)
min_size = Vector2i(600, 500)
ok_button_text = "Open"
dialog_hide_on_ok = true
file_mode = 0
filters = PackedStringArray("*.dialogue ; Dialogue")
[node name="ExportDialog" type="FileDialog" parent="."]
size = Vector2i(600, 500)
min_size = Vector2i(600, 500)
[node name="ImportDialog" type="FileDialog" parent="."]
size = Vector2i(600, 500)
min_size = Vector2i(600, 500)
filters = PackedStringArray("*.csv ; Translation CSV")
[node name="ErrorsDialog" type="AcceptDialog" parent="."]
title = "Error"
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)
min_size = Vector2i(600, 600)
ok_button_text = "Done"
[node name="SettingsView" parent="SettingsDialog" instance=ExtResource("9_8bf36")]
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
[node name="BuildErrorDialog" type="AcceptDialog" parent="."]
title = "Errors"
dialog_text = "You need to fix dialogue errors before you can run your game."
[node name="CloseConfirmationDialog" type="ConfirmationDialog" parent="."]
title = "Unsaved changes"
ok_button_text = "Save changes"
[node name="UpdatedDialog" type="AcceptDialog" parent="."]
title = "Updated"
size = Vector2i(191, 100)
dialog_text = "You're now up to date!"
[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="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"]
[connection signal="id_pressed" from="Margin/Content/SidePanel/Bookmarks/FilesList/FilesPopupMenu" to="." method="_on_files_popup_menu_id_pressed"]
[connection signal="title_selected" from="Margin/Content/SidePanel/Bookmarks/TitleList" to="." method="_on_title_list_title_selected"]
[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/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"]
[connection signal="active_title_change" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_active_title_change"]
[connection signal="caret_changed" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_caret_changed"]
[connection signal="error_clicked" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_error_clicked"]
[connection signal="external_file_requested" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_external_file_requested"]
[connection signal="text_changed" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_text_changed"]
[connection signal="error_pressed" from="Margin/Content/CodePanel/ErrorsPanel" to="." method="_on_errors_panel_error_pressed"]
[connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"]
[connection signal="file_selected" from="SaveDialog" to="." method="_on_save_dialog_file_selected"]
[connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"]
[connection signal="file_selected" from="ExportDialog" to="." method="_on_export_dialog_file_selected"]
[connection signal="file_selected" from="ImportDialog" to="." method="_on_import_dialog_file_selected"]
[connection signal="confirmed" from="SettingsDialog" to="." method="_on_settings_dialog_confirmed"]
[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"]

View File

@ -0,0 +1,144 @@
@tool
extends VBoxContainer
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 DEFAULT_TEST_SCENE_PATH = "res://addons/dialogue_manager/test_scene.tscn"
@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
@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
var editor_plugin: EditorPlugin
var all_globals: Dictionary = {}
var enabled_globals: Array = []
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")
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
revert_test_scene_button.icon = get_theme_icon("RotateLeft", "EditorIcons")
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 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))
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))
new_template_button.set_pressed_no_signal(DialogueSettings.get_setting("new_with_template", true))
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", [])
globals_list.clear()
var root = globals_list.create_item()
for name in all_globals.keys():
var item: TreeItem = globals_list.create_item(root)
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
item.set_checked(0, name in enabled_globals)
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(1, "")
globals_list.set_column_title(2, DialogueConstants.translate("settings.path"))
### Signals
func _on_settings_view_visibility_changed() -> void:
prepare()
func _on_missing_translations_button_toggled(button_pressed: bool) -> void:
DialogueSettings.set_setting("missing_translations_are_errors", button_pressed)
func _on_wrap_lines_button_toggled(button_pressed: bool) -> void:
DialogueSettings.set_setting("wrap_lines", button_pressed)
func _on_include_all_responses_button_toggled(button_pressed: bool) -> void:
DialogueSettings.set_setting("include_all_responses", button_pressed)
func _on_globals_list_item_selected() -> void:
var item = globals_list.get_selected()
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)
func _on_globals_list_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void:
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_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
func _on_load_test_scene_pressed() -> void:
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

File diff suppressed because one or more lines are too long