This commit is contained in:
Gerard Gascón 2025-04-24 17:23:34 +02:00
commit b99855351d
434 changed files with 50357 additions and 0 deletions

View file

@ -0,0 +1,146 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends InkExternalCommandExecutor
class_name InkCompiler
# ############################################################################ #
# Imports
# ############################################################################ #
var InkExecutionResult = load("res://addons/inkgd/editor/common/executors/structures/ink_execution_result.gd")
# ############################################################################ #
# Private Properties
# ############################################################################ #
## Ink Configuration
var _configuration: InkCompilationConfiguration
# ############################################################################ #
# Signals
# ############################################################################ #
## Emitted when a compilation completed. Note that this doesn't imply that
## the compulation was successful. Check the content of result
## (InkCompiler.Result) for more information.
signal story_compiled(result)
# ############################################################################ #
# Overrides
# ############################################################################ #
func _init(configuration: InkCompilationConfiguration):
_configuration = configuration
if _configuration.use_threads:
_thread = Thread.new()
# ############################################################################ #
# Methods
# ############################################################################ #
## Compile the story, based on the compilation configuration provided
## by this object. If `configuration.use_thread` is set to `false`,
## this method will return `true` if the compilation succeeded and `false`
## otherwise. If `configuration.use_thread` is set to `true`, this method
## always returns `true`.
func compile_story() -> bool:
if _configuration.use_threads:
var error = _thread.start(Callable(self, "_compile_story").bind(_configuration), Thread.PRIORITY_HIGH)
if error != OK:
var result = InkExecutionResult.new(
self.identifier,
_configuration.use_threads,
_configuration.user_triggered,
false,
""
)
call_deferred("emit_signal", "story_compiled", result)
return true
else:
return _compile_story(_configuration)
# ############################################################################ #
# Private Helpers
# ############################################################################ #
## Compile the story, based on the given compilation configuration
## If `configuration.use_thread` is set to `false`, this method will
## return `true` if the compilation succeeded and `false` otherwise.
## If `configuration.use_thread` is set to `false`, this method always
## returns `true`.
func _compile_story(config: InkCompilationConfiguration) -> bool:
print("[inkgd] [INFO] Executing compilation command…")
var return_code = 0
var output = []
var start_time = Time.get_ticks_msec()
if config.use_mono:
var args = [config.inklecate_path, '-o', config.target_file_path, config.source_file_path]
return_code = OS.execute(config.mono_path, args, output, true, false)
else:
var args = ['-o', config.target_file_path, config.source_file_path]
return_code = OS.execute(config.inklecate_path, args, output, true, false)
var end_time = Time.get_ticks_msec()
print("[inkgd] [INFO] Command executed in %dms." % (end_time - start_time))
var string_output = PackedStringArray(output)
if _configuration.use_threads:
call_deferred("_handle_compilation_result", config, return_code, string_output)
return true
else:
var result = _process_compilation_result(config, return_code, string_output)
return result.success
## Handles the compilation results when exectuted in a different thread.
##
## This method should always be executed on the main thread.
func _handle_compilation_result(
config: InkCompilationConfiguration,
return_code: int,
output: Array
):
_thread.wait_to_finish()
var result = _process_compilation_result(config, return_code, output)
emit_signal("story_compiled", result)
## Process the compilation results turning them into an instance of `Result`.
##
## This method will also print to the editor's output panel.
func _process_compilation_result(
config: InkCompilationConfiguration,
return_code: int,
output: PackedStringArray
) -> InkExecutionResult:
var success: bool = (return_code == 0)
var output_text: String = "\n".join(output).replace(BOM, "").strip_edges()
if success:
print("[inkgd] [INFO] %s was successfully compiled." % config.source_file_path)
if output_text.length() > 0:
print(output_text)
else:
printerr("[inkgd] [ERROR] Could not compile %s." % config.source_file_path)
printerr(output_text)
return InkExecutionResult.new(
self.identifier,
config.use_threads,
config.user_triggered,
success,
output_text
)

View file

@ -0,0 +1,152 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends InkExternalCommandExecutor
class_name InkConfigurationTester
# ############################################################################ #
# Imports
# ############################################################################ #
var InkExecutionResult = load("res://addons/inkgd/editor/common/executors/structures/ink_execution_result.gd")
# ############################################################################ #
# Private Properties
# ############################################################################ #
## Ink Configuration
var _configuration: InkExecutionConfiguration
# ############################################################################ #
# Signals
# ############################################################################ #
## Emitted when a test completed. Note that this doesn't imply that
## the test was successful. Check the content of result
## (InkConfigurationTester.Result) for more information.
signal availability_tested(result)
# ############################################################################ #
# Overrides
# ############################################################################ #
func _init(configuration: InkExecutionConfiguration):
_configuration = configuration
if _configuration.use_threads:
_thread = Thread.new()
# ############################################################################ #
# Methods
# ############################################################################ #
## Test inklecate's availability, based on the configuration provided by this object.
## If `configuration.use_thread` is set to `false`, this method will return
## an instance of `InkExecutionResult`, otherwise, it will return `null`.
func test_availability():
if _configuration.use_threads:
var error = _thread.start(Callable(self, "_test_availablity").bind(_configuration), Thread.PRIORITY_HIGH)
if error != OK:
var result = InkExecutionResult.new(
self.identifier,
_configuration.use_threads,
_configuration.user_triggered,
false,
""
)
emit_signal("availability_tested", result)
return true
else:
return _test_availability(_configuration)
# ############################################################################ #
# Private Helpers
# ############################################################################ #
## Test inklecate's availability, based on the configuration provided by this object
## If `configuration.use_thread` is set to `false`, this method will return
## an instance of `InkExecutionResult`, otherwise, it will return `null`.
func _test_availability(config: InkExecutionConfiguration):
print("[inkgd] [INFO] Executing test command…")
var return_code = 0
var output = []
var start_time = Time.get_ticks_msec()
if config.use_mono:
var args = [config.inklecate_path]
return_code = OS.execute(config.mono_path, args, output, true, false)
else:
return_code = OS.execute(config.inklecate_path, [], output, true, false)
var end_time = Time.get_ticks_msec()
print("[inkgd] [INFO] Command executed in %dms." % (end_time - start_time))
var string_output = PackedStringArray(output)
if _configuration.use_threads:
call_deferred("_handle_test_result", config, return_code, string_output)
return null
else:
return _process_test_result(config, return_code, string_output)
## Handles the test result when exectuted in a different thread.
##
## This method should always be executed on the main thread.
func _handle_test_result(config: InkExecutionConfiguration, return_code: int, output: Array):
_thread.wait_to_finish()
var result = _process_test_result(config, return_code, output)
emit_signal("availability_tested", result)
## Process the compilation results turning them into an instance of `Result`.
##
## This method will also print to the editor's output panel.
func _process_test_result(
config: InkExecutionConfiguration,
return_code: int,
output: PackedStringArray
) -> InkExecutionResult:
var success: bool = (return_code == 0 || _contains_inklecate_output_prefix(output))
var output_text: String = "\n".join(output).replace(BOM, "").strip_edges()
if success:
if !output_text.is_empty():
print("[inkgd] [INFO] inklecate was found and executed:")
print(output_text)
else:
print("[inkgd] [INFO] inklecate was found and executed.")
else:
printerr("[inkgd] [ERROR] Something went wrong while testing inklecate's setup.")
printerr(output_text)
return InkExecutionResult.new(
self.identifier,
config.use_threads,
config.user_triggered,
success,
output_text
)
## Guess whether the provided `output_array` looks like the usage inklecate
## outputs when run with no parameters.
func _contains_inklecate_output_prefix(output_array: PackedStringArray):
# No valid output -> it's not inklecate.
if output_array.size() == 0: return false
# The first line of the output is cleaned up by removing the BOM and
# any sort of whitespace/unprintable character.
var cleaned_line = output_array[0].replace(BOM, "").strip_edges()
# If the first line starts with the correct substring, it's likely
# to be inklecate!
return cleaned_line.find("Usage: inklecate2") == 0

View file

@ -0,0 +1,32 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends RefCounted
class_name InkExternalCommandExecutor
# ############################################################################ #
# Properties
# ############################################################################ #
## The identifier of this compiler.
var identifier: int: get = get_identifier
func get_identifier() -> int:
return get_instance_id()
# ############################################################################ #
# Constants
# ############################################################################ #
const BOM = "\ufeff"
# ############################################################################ #
# Private Properties
# ############################################################################ #
## Thread used to compile the story.
@warning_ignore("unused_private_class_variable") # Used by subclasses.
var _thread: Thread

View file

@ -0,0 +1,44 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends InkExecutionConfiguration
## Contains all the configuration settings necessary to perform a compilation.
class_name InkCompilationConfiguration
# ############################################################################ #
# Properties
# ############################################################################ #
## The path to the story to compile, local to the file system.
var source_file_path: String = ""
## The path to the compiled story, local to the file system.
var target_file_path: String = ""
# ############################################################################ #
# Overrides
# ############################################################################ #
@warning_ignore("shadowed_variable")
func _init(
configuration: InkConfiguration,
use_threads: bool,
user_triggered: bool,
source_file_path: String,
target_file_path: String
):
super(configuration, use_threads, user_triggered)
self.source_file_path = ProjectSettings.globalize_path(source_file_path)
self.target_file_path = ProjectSettings.globalize_path(target_file_path)
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _is_running_on_windows():
var os_name = OS.get_name()
return (os_name == "Windows" || os_name == "UWP")

View file

@ -0,0 +1,49 @@
@tool
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends RefCounted
## Contains all the configuration settings necessary to perform an execution.
class_name InkExecutionConfiguration
# ############################################################################ #
# Properties
# ############################################################################ #
var use_threads: bool = false
var user_triggered: bool = false
var use_mono: bool = false
var mono_path: String = ""
var inklecate_path: String = ""
# ############################################################################ #
# Overrides
# ############################################################################ #
@warning_ignore("shadowed_variable")
func _init(
configuration: InkConfiguration,
use_threads: bool,
user_triggered: bool
):
self.use_threads = use_threads
self.user_triggered = user_triggered
self.use_mono = !_is_running_on_windows() && configuration.use_mono
self.mono_path = configuration.mono_path
self.inklecate_path = configuration.inklecate_path
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _is_running_on_windows():
var os_name = OS.get_name()
return (os_name == "Windows" || os_name == "UWP")

View file

@ -0,0 +1,44 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends RefCounted
## A test result, containing information about whether the test
## suceeded and the generated output.
class_name InkExecutionResult
# ############################################################################ #
# Properties
# ############################################################################ #
## The identifier of the compiler that generated this result.
## This is the value of 'InkExecutor.identifier'.
var identifier: int = 0
var use_threads: bool = false
var user_triggered: bool = false
var success: bool = false
var output: String = ""
# ############################################################################ #
# Overrides
# ############################################################################ #
@warning_ignore("shadowed_variable")
func _init(
identifier: int,
use_threads: bool,
user_triggered: bool,
success: bool,
output: String
):
self.identifier = identifier
self.use_threads = use_threads
self.user_triggered = user_triggered
self.success = success
self.output = output

View file

@ -0,0 +1,204 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends RefCounted
class_name InkConfiguration
# ############################################################################ #
# Enums
# ############################################################################ #
enum BuildMode {
MANUAL = 0,
DURING_BUILD,
AFTER_CHANGE
}
# ############################################################################ #
# Constants
# ############################################################################ #
const ROOT_DIR = "res://"
const COMPILER_CONFIG = ROOT_DIR + ".inkgd_compiler.cfg"
const INK_CONFIG = ROOT_DIR + ".inkgd_ink.cfg"
const COMPILER_CONFIG_FORMAT_VERSION = 2
const INK_CONFIG_FORMAT_VERSION = 2
const FORMAT_SECTION = "format"
const VERSION = "version"
const INKGD_SECTION = "inkgd"
const USE_MONO = "use_mono"
const MONO_PATH = "mono_path"
const INKLECATE_PATH = "inklecate_path"
const COMPILATION_MODE = "compilation_mode"
const STORIES = "stories"
const SOURCE_FILE_PATH = "source_file_path"
const TARGET_FILE_PATH = "target_file_path"
const WATCHED_FOLDER_PATH = "watched_folder_path"
const DEFAULT_STORIES = [
{
SOURCE_FILE_PATH: "",
TARGET_FILE_PATH: "",
WATCHED_FOLDER_PATH: ""
}
]
# ############################################################################ #
# Signals
# ############################################################################ #
signal story_configuration_changed()
signal compilation_mode_changed(compilation_mode)
# ############################################################################ #
# Properties
# ############################################################################ #
var use_mono: bool = false
var mono_path: String = ""
var inklecate_path: String = ""
var compilation_mode: int = BuildMode.MANUAL: set = set_compilation_mode
func set_compilation_mode(new_value: int):
compilation_mode = new_value
emit_signal("compilation_mode_changed", compilation_mode)
var stories: Array = DEFAULT_STORIES
# ############################################################################ #
# Private Properties
# ############################################################################ #
var _compiler_config_file = ConfigFile.new()
var _ink_config_file = ConfigFile.new()
# ############################################################################ #
# Overrides
# ############################################################################ #
func _init():
pass
# ############################################################################ #
# Public Methods
# ############################################################################ #
## Loads the content of the configuration files from disk.
func retrieve():
_retrieve_inklecate()
_retrieve_ink()
## Stores the content of the configuration to the disk.
func persist():
_persist_inklecate()
_persist_ink()
func append_new_story_configuration(
source_file_path: String,
target_file_path: String,
wacthed_folder_path: String
):
stories.append({
SOURCE_FILE_PATH: source_file_path,
TARGET_FILE_PATH: target_file_path,
WATCHED_FOLDER_PATH: wacthed_folder_path
})
emit_signal("story_configuration_changed")
func remove_story_configuration_at_index(index: int):
if index >= 0 && index < stories.size():
stories.remove_at(index)
emit_signal("story_configuration_changed")
func get_story_configuration_at_index(index):
if index >= 0 && index < stories.size():
return stories[index]
else:
return null
func get_source_file_path(story_configuration):
return story_configuration[SOURCE_FILE_PATH]
func get_target_file_path(story_configuration):
return story_configuration[TARGET_FILE_PATH]
func get_watched_folder_path(story_configuration):
return story_configuration[WATCHED_FOLDER_PATH]
# ############################################################################ #
# Private Methods
# ############################################################################ #
## Loads the content of the inklecate configuration file from disk.
func _retrieve_inklecate():
var err = _compiler_config_file.load(COMPILER_CONFIG)
if err != OK:
# Assuming it doesn't exist.
return
use_mono = _compiler_config_file.get_value(INKGD_SECTION, USE_MONO, false)
mono_path = _compiler_config_file.get_value(INKGD_SECTION, MONO_PATH, "")
inklecate_path = _compiler_config_file.get_value(INKGD_SECTION, INKLECATE_PATH, "")
if _compiler_config_file.get_value(FORMAT_SECTION, VERSION, 0) >= 2:
compilation_mode = _compiler_config_file.get_value(INKGD_SECTION, COMPILATION_MODE, 0)
## Loads the content of the story configuration file from disk.
func _retrieve_ink():
var err = _ink_config_file.load(INK_CONFIG)
if err != OK:
# Assuming it doesn't exist.
return
if _ink_config_file.get_value(FORMAT_SECTION, VERSION, 0) >= 2:
stories = _ink_config_file.get_value(INKGD_SECTION, STORIES, DEFAULT_STORIES)
else:
var source_file_path = _ink_config_file.get_value(INKGD_SECTION, SOURCE_FILE_PATH, "")
var target_file_path = _ink_config_file.get_value(INKGD_SECTION, TARGET_FILE_PATH, "")
var watched_folder_path = _ink_config_file.get_value(INKGD_SECTION, WATCHED_FOLDER_PATH, "")
stories[0] = {
SOURCE_FILE_PATH: source_file_path,
TARGET_FILE_PATH: target_file_path,
WATCHED_FOLDER_PATH: watched_folder_path
}
## Stores the content of the inklecate configuration to the disk.
func _persist_inklecate():
_compiler_config_file.set_value(FORMAT_SECTION, VERSION, COMPILER_CONFIG_FORMAT_VERSION)
_compiler_config_file.set_value(INKGD_SECTION, USE_MONO, use_mono)
_compiler_config_file.set_value(INKGD_SECTION, MONO_PATH, mono_path)
_compiler_config_file.set_value(INKGD_SECTION, INKLECATE_PATH, inklecate_path)
_compiler_config_file.set_value(INKGD_SECTION, COMPILATION_MODE, compilation_mode)
var err = _compiler_config_file.save(COMPILER_CONFIG)
if err != OK:
printerr("[inkgd] [ERROR] Could not save: %s" % COMPILER_CONFIG)
## Stores the content of the story configuration to the disk.
func _persist_ink():
# Clean up the file if it was created before version 2.
if _ink_config_file.has_section_key(INKGD_SECTION, SOURCE_FILE_PATH):
_ink_config_file.erase_section_key(INKGD_SECTION, SOURCE_FILE_PATH)
if _ink_config_file.has_section_key(INKGD_SECTION, TARGET_FILE_PATH):
_ink_config_file.erase_section_key(INKGD_SECTION, TARGET_FILE_PATH)
# Write version 2 values.
_ink_config_file.set_value(FORMAT_SECTION, VERSION, COMPILER_CONFIG_FORMAT_VERSION)
_ink_config_file.set_value(INKGD_SECTION, STORIES, stories)
var err = _ink_config_file.save(INK_CONFIG)
if err != OK:
printerr("[inkgd] [ERROR] Could not save: %s" % INK_CONFIG)

View file

@ -0,0 +1,103 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends RefCounted
# A crude validator catching the most common mistakes.
class_name InkCSharpValidator
const INK_ENGINE_RUNTIME = "ink-engine-runtime.dll"
# ############################################################################ #
# Methods
# ############################################################################ #
func validate_csharp_project_files(project_name) -> bool:
var ink_engine_runtime := get_runtime_path()
if ink_engine_runtime.is_empty():
print(
"[inkgd] [INFO] 'ink-engine-runtime.dll' seems to be missing " +
"from the project. If you encounter errors while building the " +
"solution, please refer to [TO BE ADDED] for help."
)
return false
return _validate_csproj(project_name, ink_engine_runtime)
func get_runtime_path() -> String:
return _scan_directory("res://")
func _validate_csproj(project_name: String, runtime_path: String) -> bool:
var csproj_path = "res://%s.csproj" % project_name
if !FileAccess.file_exists(csproj_path):
printerr(
("[inkgd] [ERROR] The C# project (%s.csproj) doesn't exist. " % project_name) +
"You can create a new C# project through " +
"Project > Tools > C# > Create C# Solution. Alternatively, you can also set " +
"Project Settings > General > Inkgd > Do Not Use Mono Runtime to 'Yes' " +
"if you do not wish to use the C# version of Ink. "
)
return false
var file := FileAccess.open(csproj_path, FileAccess.READ)
var error := FileAccess.get_open_error()
if error != OK:
printerr(
"[inkgd] [ERROR] The C# project (%s.csproj) exists but it could not be opened." +
"(Code %d)" % [project_name, error]
)
return false
var content := file.get_as_text()
file.close()
if content.find(runtime_path.replace("res://", "")) == -1:
print(
"[inkgd] [INFO] '%s.csproj' seems to be missing a " % project_name +
"<RefCounted> item matching '%s'. If you encounter " % runtime_path +
"further errors please refer to [TO BE ADDED] for help."
)
return false
print("[inkgd] [INFO] The C# Project seems to be configured correctly.")
return true
func _scan_directory(path) -> String:
var directory := DirAccess.open(path)
var error := DirAccess.get_open_error()
if error != OK:
printerr(
"[inkgd] [ERROR] Could not open '%s', " % path +
"can't look for ink-engine-runtime.dll."
)
return ""
if directory.list_dir_begin() != OK:# TODOConverter3To4 fill missing arguments https://github.com/godotengine/godot/pull/40547
printerr(
"[inkgd] [ERROR] Could not list contents of '%s', " % path +
"can't look for ink-engine-runtime.dll."
)
return ""
var file_name := directory.get_next()
while file_name != "":
if directory.current_is_dir():
var ink_runtime = _scan_directory(
"%s/%s" % [directory.get_current_dir(), file_name]
)
if !ink_runtime.is_empty():
return ink_runtime
else:
if file_name == INK_ENGINE_RUNTIME:
return "%s/%s" % [directory.get_current_dir(), file_name]
file_name = directory.get_next()
return ""

View file

@ -0,0 +1,84 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends RefCounted
class_name InkEditorInterface
# ############################################################################ #
# Signals
# ############################################################################ #
## Emitted when 'Ink' resources (i. e. files with the '.ink' extension) were
## reimported by Godot.
signal ink_ressources_reimported(resources)
# ############################################################################ #
# Properties
# ############################################################################ #
## The pixel display scale of the editor.
var scale: float = 1.0
var editor_interface: EditorInterface
var editor_settings: EditorSettings
var editor_filesystem: EditorFileSystem
## `true` if the editor is running on Windows, `false` otherwise.
var is_running_on_windows: bool: get = get_is_running_on_windows
func get_is_running_on_windows() -> bool:
var os_name = OS.get_name()
return (os_name == "Windows" || os_name == "UWP")
# ############################################################################ #
# Overrides
# ############################################################################ #
@warning_ignore("shadowed_variable")
func _init(editor_interface: EditorInterface):
self.editor_interface = editor_interface
self.editor_settings = editor_interface.get_editor_settings()
self.editor_filesystem = editor_interface.get_resource_filesystem()
scale = editor_interface.get_editor_scale()
self.editor_filesystem.connect("resources_reimported", Callable(self, "_resources_reimported"))
# ############################################################################ #
# Methods
# ############################################################################ #
## Tell Godot to scan for updated resources.
func scan_file_system():
self.editor_filesystem.scan()
## Tell Godot to scan the given resource.
func update_file(path: String):
self.editor_filesystem.update_file(path)
## Returns a custom header color based on the editor's base color.
##
## If the base color is not found, return 'Color.transparent'.
func get_custom_header_color() -> Color:
var color = self.editor_settings.get_setting("interface/theme/base_color")
if color != null:
return Color.from_hsv(color.h * 0.99, color.s * 0.6, color.v * 1.1)
else:
return Color.TRANSPARENT
# ############################################################################ #
# Signal Receivers
# ############################################################################ #
func _resources_reimported(resources):
var ink_resources := PackedStringArray()
for resource in resources:
if resource.get_extension() == "ink":
ink_resources.append(resource)
emit_signal("ink_ressources_reimported", ink_resources)

BIN
addons/inkgd/editor/icons/compile.svg (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b3mk2i51yhbcn"
path="res://.godot/imported/compile.svg-8aaa0400caa294ef4be4b83b3d6de9aa.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/inkgd/editor/icons/compile.svg"
dest_files=["res://.godot/imported/compile.svg-8aaa0400caa294ef4be4b83b3d6de9aa.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

BIN
addons/inkgd/editor/icons/ink_player.svg (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://tq48ux26onni"
path="res://.godot/imported/ink_player.svg-69488eb4b789a6c4d2f7a8b28953ad01.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/inkgd/editor/icons/ink_player.svg"
dest_files=["res://.godot/imported/ink_player.svg-69488eb4b789a6c4d2f7a8b28953ad01.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,94 @@
# ############################################################################ #
# Copyright © 2018-2022 Paul Joannon
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends EditorImportPlugin
class_name InkJsonImportPlugin
# ############################################################################ #
# Imports
# ############################################################################ #
var InkConfiguration = load("res://addons/inkgd/editor/common/ink_configuration.gd")
var InkResource = load("res://addons/inkgd/editor/import_plugins/ink_resource.gd")
# ############################################################################ #
# Properties
# ############################################################################ #
var _configuration = InkConfiguration.new()
# ############################################################################ #
# Overrides
# ############################################################################ #
func _get_importer_name():
return "inkgd.ink.json";
func _get_visible_name():
return "Compiled ink story";
func _get_recognized_extensions():
return ["json"];
func _get_save_extension():
return "res";
func _get_resource_type():
return "Resource";
func _get_priority():
return 1.0
func _get_import_options(_path, _preset):
return [
{
"name": "compress",
"default_value": true
}
]
func _get_import_order():
return 0
func _get_option_visibility(_path, _option_name, _options):
return true
func _get_preset_count():
return 0
func _import(source_file, save_path, options, _platform_variants, _gen_files):
_configuration.retrieve()
var raw_json = _get_file_content(source_file)
var test_json_conv = JSON.new()
test_json_conv.parse(raw_json)
var json = test_json_conv.get_data()
if !json.has("inkVersion"):
return ERR_FILE_UNRECOGNIZED
var resource = InkResource.new()
resource.json = raw_json
var flags = ResourceSaver.FLAG_COMPRESS if options["compress"] else 0
return ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()], flags)
# ############################################################################ #
# Private Helpers
# ############################################################################ #
func _get_file_content(source_file):
var file := FileAccess.open(source_file, FileAccess.READ)
var err := FileAccess.get_open_error()
if err != OK:
return err
var text_content = file.get_as_text()
file.close()
return text_content

View file

@ -0,0 +1,16 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends Resource
# A very simple resource to store the content of a json file, as a string.
class_name InkResource
# ############################################################################ #
# Properties
# ############################################################################ #
@export var json: String = ""

View file

@ -0,0 +1,51 @@
# ############################################################################ #
# Copyright © 2018-2022 Paul Joannon
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends EditorImportPlugin
class_name InkSourceImportPlugin
# ############################################################################ #
# Overrides
# ############################################################################ #
func _get_importer_name():
return "inkgd.ink";
func _get_visible_name():
return "Ink file";
func _get_recognized_extensions():
return ["ink"];
func _get_save_extension():
return "res";
func _get_resource_type():
return "Resource";
func _get_priority():
return 1.0
func _get_import_options(_path, _preset):
return []
func _get_import_order():
return 0
func _get_option_visibility(_path, _option_name, _options):
return true
func _get_preset_count():
return 0
func _import(_source_file, save_path, _options, _platform_variants, _gen_files):
return ResourceSaver.save(
Resource.new(),
"%s.%s" % [save_path, _get_save_extension()],
ResourceSaver.FLAG_COMPRESS
)

View file

@ -0,0 +1,283 @@
@tool
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends EditorPlugin
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkEditorPlugin
# ############################################################################ #
# Imports
# ############################################################################ #
var InkJsonImportPlugin = preload("res://addons/inkgd/editor/import_plugins/ink_json_import_plugin.gd")
var InkSourceImportPlugin = preload("res://addons/inkgd/editor/import_plugins/ink_source_import_plugin.gd")
var InkBottomPanel = preload("res://addons/inkgd/editor/panel/ink_bottom_panel.tscn")
var InkCSharpValidator = preload("res://addons/inkgd/editor/common/ink_csharp_validator.gd")
var InkEditorInterface = load("res://addons/inkgd/editor/common/ink_editor_interface.gd")
var InkConfiguration = load("res://addons/inkgd/editor/common/ink_configuration.gd")
var InkCompilationConfiguration = load("res://addons/inkgd/editor/common/executors/structures/ink_compilation_configuration.gd")
var InkCompiler = load("res://addons/inkgd/editor/common/executors/ink_compiler.gd")
# ############################################################################ #
# Constant
# ############################################################################ #
const USE_MONO_RUNTIME_SETTING = "inkgd/use_mono_runtime"
const REGISTER_TEMPLATES_SETTING = "inkgd/register_templates"
# ############################################################################ #
# Private Properties
# ############################################################################ #
var _editor_interface: InkEditorInterface = null
var _configuration: InkConfiguration = null
var _panel = null
var _ink_source_import_plugin: InkSourceImportPlugin = null
var _ink_json_import_plugin: InkJsonImportPlugin = null
var _tool_button: Button = null
# ############################################################################ #
# Overrides
# ############################################################################ #
func _enter_tree():
var ink_player_icon = load("res://addons/inkgd/editor/icons/ink_player.svg")
if ink_player_icon == null:
printerr(
"[inkgd] [ERROR] The plugin could not be initialized because the required assets " +
"haven't been imported by Godot yet. This can happen when cloning a fresh project or " +
"after deleting the '.import' folder. Disabling and reenabling InkGD in " +
"Project > Project setting… > Plugins or reloading the project should fix the problem."
)
return
# Note: assets are not preloaded to prevent the script from failing
# its interpretation phase if the resources have never been imported before.
if _should_use_mono() && _validate_csproj():
print("[inkgd] [INFO] Using the Mono runtime.")
_register_custom_settings()
add_custom_type(
"InkPlayer",
"Node",
load("res://addons/inkgd/mono/InkPlayer.cs"),
ink_player_icon
)
else:
print("[inkgd] [INFO] Using the GDScript runtime.")
_register_custom_settings()
add_custom_type(
"InkPlayer",
"Node",
load("res://addons/inkgd/ink_player.gd"),
ink_player_icon
)
_editor_interface = InkEditorInterface.new(get_editor_interface())
_configuration = InkConfiguration.new()
_configuration.retrieve()
_add_bottom_panel()
_add_import_plugin()
_add_autoloads()
_add_templates()
func _exit_tree():
# The plugin hasn't been intialised properly, nothing to do.
if _panel == null:
return
_remove_bottom_panel()
_remove_import_plugin()
_remove_autoloads()
_remove_templates()
remove_custom_type("InkPlayer")
func build():
if _configuration.compilation_mode == InkConfiguration.BuildMode.DURING_BUILD:
var previous_result = true
for story_configuration in _configuration.stories:
if !previous_result:
break
var source_file_path = _configuration.get_source_file_path(story_configuration)
var target_file_path = _configuration.get_target_file_path(story_configuration)
var compiler_configuration = InkCompilationConfiguration.new(
_configuration,
false,
false,
source_file_path,
target_file_path
)
var compiler = InkCompiler.new(compiler_configuration)
var current_result = compiler.compile_story()
if current_result:
_editor_interface.call_deferred("update_file", target_file_path)
previous_result = previous_result && current_result
return previous_result
else:
return true
# ############################################################################ #
# Private Helpers
# ############################################################################ #
func _add_import_plugin():
_ink_source_import_plugin = InkSourceImportPlugin.new()
_ink_json_import_plugin = InkJsonImportPlugin.new()
add_import_plugin(_ink_source_import_plugin)
add_import_plugin(_ink_json_import_plugin)
func _remove_import_plugin():
remove_import_plugin(_ink_source_import_plugin)
remove_import_plugin(_ink_json_import_plugin)
_ink_source_import_plugin = null
_ink_json_import_plugin = null
func _add_bottom_panel():
_panel = InkBottomPanel.instantiate()
_panel.editor_interface = _editor_interface
_panel.configuration = _configuration
_tool_button = add_control_to_bottom_panel(_panel, "Ink")
func _remove_bottom_panel():
remove_control_from_bottom_panel(_panel)
_panel.queue_free()
## Registers the Ink runtime node as an autoloaded singleton.
func _add_autoloads():
add_autoload_singleton("__InkRuntime", "res://addons/inkgd/runtime/static/ink_runtime.gd")
## Unregisters the Ink runtime node from autoloaded singletons.
func _remove_autoloads():
remove_autoload_singleton("__InkRuntime")
## Registers the script templates provided by the plugin.
func _add_templates():
if ProjectSettings.has_setting(REGISTER_TEMPLATES_SETTING):
var register_template = ProjectSettings.get_setting(REGISTER_TEMPLATES_SETTING)
if !register_template: return
var names = _get_plugin_templates_names()
# Setup the templates folder for the project
var template_dir_path = ProjectSettings.get_setting("editor/script/templates_search_path")
if !DirAccess.dir_exists_absolute(template_dir_path):
DirAccess.make_dir_absolute(template_dir_path)
for template_name in names:
var template_file_path = template_dir_path + "/" + template_name
DirAccess.copy_absolute("res://addons/inkgd/editor/templates/" + template_name, template_file_path)
## Unregisters the script templates provided by the plugin.
func _remove_templates():
var names = _get_plugin_templates_names()
var template_dir_path = ProjectSettings.get_setting("editor/script/templates_search_path")
for template_name in names:
var template_file_path = template_dir_path + "/" + template_name
if FileAccess.file_exists(template_file_path):
DirAccess.remove_absolute(template_file_path)
## Get all the script templates provided by the plugin.
func _get_plugin_templates_names() -> Array:
var plugin_template_names = []
var dir = DirAccess.open("res://addons/inkgd/editor/templates/")
if dir:
dir.list_dir_begin()
var temp = dir.get_next()
while temp != "":
plugin_template_names.append(temp)
temp = dir.get_next()
return plugin_template_names
else:
print("An error occurred when trying to access the path.")
return []
func _register_custom_settings():
if _can_run_mono():
if !ProjectSettings.has_setting(USE_MONO_RUNTIME_SETTING):
ProjectSettings.set_setting(USE_MONO_RUNTIME_SETTING, true)
var mono_property_info = {
"name": USE_MONO_RUNTIME_SETTING,
"type": TYPE_BOOL,
"hint_string": "If `true` _inkgd_ will alwaus use the Mono runtime when available.",
"default": false
}
ProjectSettings.add_property_info(mono_property_info)
if !ProjectSettings.has_setting(REGISTER_TEMPLATES_SETTING):
ProjectSettings.set_setting(REGISTER_TEMPLATES_SETTING, true)
var template_property_info = {
"name": REGISTER_TEMPLATES_SETTING,
"type": TYPE_BOOL,
"hint_string": "If `true` _inkgd_ will register its script templates with the current project.",
"default": false
}
ProjectSettings.add_property_info(template_property_info)
func _validate_csproj() -> bool:
var project_name = ProjectSettings.get_setting("application/config/name")
if project_name.is_empty():
printerr("[inkgd] [ERROR] The project is missing a name.")
return false
var validator = InkCSharpValidator.new()
return validator.validate_csharp_project_files(project_name)
func _should_use_mono():
if ProjectSettings.has_setting(USE_MONO_RUNTIME_SETTING):
var use_mono = ProjectSettings.get_setting(USE_MONO_RUNTIME_SETTING)
if use_mono == null:
use_mono = true
return _can_run_mono() && use_mono
else:
return _can_run_mono()
func _can_run_mono():
return type_exists("_GodotSharp")

View file

@ -0,0 +1,57 @@
@tool
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends Popup
# A custom dialog showing a progress bar.
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkProgressDialog
# ############################################################################ #
# Nodes
# ############################################################################ #
@onready var _margin_container = $MarginContainer
@onready var _vbox_container = $MarginContainer/VBoxContainer
@onready var _title_label = $MarginContainer/VBoxContainer/TitleLabel
@onready var _progress_bar = $MarginContainer/VBoxContainer/ProgressBar
@onready var _current_step_label = $MarginContainer/VBoxContainer/CurrentStepLabel
# ############################################################################ #
# Properties
# ############################################################################ #
## The title of the progress.
var progress_title: String: get = get_progress_title, set = set_progress_title
func set_progress_title(text: String):
_title_label.text = text
func get_progress_title() -> String:
return _title_label.text
## The name of the current step.
var current_step_name: String: get = get_current_step_name, set = set_current_step_name
func set_current_step_name(text: String):
_current_step_label.text = text
func get_current_step_name() -> String:
return _current_step_label.text
## The current progress.
var progress: float:
get:
return _progress_bar.value
set(value):
_progress_bar.value = value
func update_layout(scale: float) -> void:
_margin_container.add_theme_constant_override("offset_right", 10 * scale)
_margin_container.add_theme_constant_override("offset_top", 10 * scale)
_margin_container.add_theme_constant_override("offset_left", 10 * scale)
_margin_container.add_theme_constant_override("offset_bottom", 10 * scale)
_vbox_container.add_theme_constant_override("separation", 5 * scale)

View file

@ -0,0 +1,63 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/inkgd/editor/panel/common/ink_progress_dialog.gd" type="Script" id=1]
[node name="InkProgressDialog" type="Popup"]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -250.0
offset_top = -42.5
offset_right = 250.0
offset_bottom = 42.5
custom_minimum_size = Vector2( 500, 85 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="MarginContainer" type="MarginContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_constants/margin_right = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_left = 10
theme_override_constants/margin_bottom = 10
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
offset_left = 10.0
offset_top = 10.0
offset_right = 490.0
offset_bottom = 75.0
theme_override_constants/separation = 5
alignment = 1
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer"]
offset_top = 1.0
offset_right = 480.0
offset_bottom = 15.0
text = "Compiling..."
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ProgressBar" type="ProgressBar" parent="MarginContainer/VBoxContainer"]
offset_top = 25.0
offset_right = 480.0
offset_bottom = 39.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="CurrentStepLabel" type="Label" parent="MarginContainer/VBoxContainer"]
offset_top = 49.0
offset_right = 480.0
offset_bottom = 63.0
text = "the_intercept.ink"

View file

@ -0,0 +1,103 @@
@tool
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends Window
# A custom dialog showing a message and, optionally, a command output.
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkRichDialog
# ############################################################################ #
# Nodes
# ############################################################################ #
@onready var _margin_container = $MarginContainer
@onready var _vbox_container = $MarginContainer/VBoxContainer
@onready var _message_label = $MarginContainer/VBoxContainer/MessageLabel
@onready var _accept_button = $MarginContainer/VBoxContainer/AcceptButton
@onready var _output_panel = $MarginContainer/VBoxContainer/OutputPanel
@onready var _output_label = find_child("OutputLabel")
# ############################################################################ #
# Properties
# ############################################################################ #
## The message displayed in the dialog.
var message_text: String: get = get_message_text, set = set_message_text
func set_message_text(text: String):
_message_label.text = text
func get_message_text() -> String:
return _message_label.text
## An output, often the result of a command, than can optionally be displayed
## in the dialog.
##
## Setting this property to null hides the corresponding panel in the dialog.
var output_text: String: get = get_output_text, set = set_output_text
func set_output_text(text: String):
_output_label.text = text
_output_label.visible = !(text == null || text.length() == 0)
func get_output_text() -> String:
return _output_label.text
# ############################################################################ #
# Overriden Methods
# ############################################################################ #
func _ready():
_accept_button.connect("pressed", Callable(self, "_accept_button_pressed"))
var font = _get_source_font()
if font != null:
_output_panel.add_theme_font_override("font", font)
# ############################################################################ #
# Methods
# ############################################################################ #
func update_layout(scale: float) -> void:
_margin_container.add_theme_constant_override("offset_right", 10 * scale)
_margin_container.add_theme_constant_override("offset_top", 10 * scale)
_margin_container.add_theme_constant_override("offset_left", 10 * scale)
_margin_container.add_theme_constant_override("offset_bottom", 10 * scale)
_vbox_container.add_theme_constant_override("separation", 10 * scale)
# ############################################################################ #
# Signal Receivers
# ############################################################################ #
func _accept_button_pressed():
self.get_parent().remove_child(self)
self.queue_free()
# ############################################################################ #
# Private helpers
# ############################################################################ #
## Gets the monospaced font used by the editor.
func _get_source_font():
var base_theme = _retrieve_base_theme()
if base_theme:
return base_theme.get_font("output_source", "EditorFonts")
else:
return null
## Gets the theme currently used by the editor.
func _retrieve_base_theme():
var parent = self
while(parent != null && parent.theme == null):
var older_parent = parent.get_parent()
if older_parent is Control:
parent = older_parent
else:
break
return parent.theme

View file

@ -0,0 +1,89 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/inkgd/editor/panel/common/ink_rich_dialog.gd" type="Script" id=1]
[node name="Window" type="Window"]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -250.0
offset_top = -150.0
offset_right = 250.0
offset_bottom = 150.0
custom_minimum_size = Vector2( 500, 300 )
window_title = "Error"
resizable = true
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="MarginContainer" type="MarginContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/margin_right = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_left = 10
theme_override_constants/margin_bottom = 10
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
offset_left = 10.0
offset_top = 10.0
offset_right = 490.0
offset_bottom = 290.0
theme_override_constants/separation = 10
[node name="MessageLabel" type="Label" parent="MarginContainer/VBoxContainer"]
offset_right = 480.0
offset_bottom = 31.0
text = "Something went wrong while testing inklecate's setup. Please see the output below."
autowrap = true
[node name="OutputPanel" type="Panel" parent="MarginContainer/VBoxContainer"]
offset_top = 41.0
offset_right = 480.0
offset_bottom = 250.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="OutputScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/OutputPanel"]
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="OutputMarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/OutputPanel/OutputScrollContainer"]
offset_right = 480.0
offset_bottom = 136.0
size_flags_horizontal = 3
theme_override_constants/margin_right = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_left = 10
theme_override_constants/margin_bottom = 10
__meta__ = {
"_edit_use_anchors_": false
}
[node name="OutputLabel" type="Label" parent="MarginContainer/VBoxContainer/OutputPanel/OutputScrollContainer/OutputMarginContainer"]
offset_left = 10.0
offset_top = 10.0
offset_right = 470.0
offset_bottom = 126.0
size_flags_vertical = 0
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
autowrap = true
[node name="AcceptButton" type="Button" parent="MarginContainer/VBoxContainer"]
offset_left = 224.0
offset_top = 260.0
offset_right = 255.0
offset_bottom = 280.0
size_flags_horizontal = 4
text = "OK"

View file

@ -0,0 +1,306 @@
@tool
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends Control
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkConfigurationPanel
# ############################################################################ #
# Imports
# ############################################################################ #
var InkExecutionConfiguration = load("res://addons/inkgd/editor/common/executors/structures/ink_execution_configuration.gd")
var InkConfigurationTester = load("res://addons/inkgd/editor/common/executors/ink_configuration_tester.gd")
var InkCSharpValidator = preload("res://addons/inkgd/editor/common/ink_csharp_validator.gd")
var InkRichDialog = load("res://addons/inkgd/editor/panel/common/ink_rich_dialog.tscn")
# ############################################################################ #
# Enums
# ############################################################################ #
## Represents which configuration setting triggered the file dialog.
enum FileDialogSelection {
UNKNOWN,
MONO_EXECUTABLE,
INKLECATE_EXECUTABLE
}
# ############################################################################ #
# Constants
# ############################################################################ #
const BOM = "\ufeff"
# ############################################################################ #
# Properties
# ############################################################################ #
var editor_interface: InkEditorInterface = null
var configuration: InkConfiguration = null
# ############################################################################ #
# Private Properties
# ############################################################################ #
var _file_dialog = EditorFileDialog.new()
## Configuration item for which the FileDialog is currently shown.
##
## Unknown by default.
var _file_dialog_selection = FileDialogSelection.UNKNOWN
# ############################################################################ #
# Nodes
# ############################################################################ #
@onready var _test_button = find_child("TestButton")
@onready var _use_mono_label = find_child("UseMonoLabel")
@onready var _use_mono_checkbox = find_child("UseMonoCheckBox")
@onready var _mono_label = find_child("MonoLabel")
@onready var _mono_container = find_child("MonoH")
@onready var _mono_line_edit = find_child("MonoLineEdit")
@onready var _mono_dialog_button = find_child("MonoDialogButton")
@onready var _executable_line_edit = find_child("ExecutableLineEdit")
@onready var _executable_dialog_button = find_child("ExecutableDialogButton")
@onready var _recompilation_mode_button = find_child("RecompilationModeOptionButton")
@onready var _mono_support_container = find_child("MonoSupportV")
@onready var _mono_support_documentation_button = find_child("DocumentationButton")
@onready var _mono_support_presence_label = _mono_support_container.find_child("PresenceLabel")
@onready var _mono_support_refresh_button = _mono_support_container.find_child("RefreshButton")
# ############################################################################ #
# Overrides
# ############################################################################ #
func _ready():
# FIXME: This needs investigating.
# Sanity check. It seems the editor instantiates tools script on their
# own, probably to add them to its tree. In that case, they won't have
# their dependencies injected, so we're not doing anything.
if editor_interface == null || configuration == null:
print("[inkgd] [INFO] Ink Configuration Tab: dependencies not met, ignoring.")
return
_set_button_icons()
_apply_configuration()
_connect_signals()
_check_runtime_presence()
_mono_support_container.visible = _can_run_mono()
add_child(_file_dialog)
# ############################################################################ #
# Signal Receivers
# ############################################################################ #
func _configuration_entered(_new_text: String):
_configuration_focus_exited()
func _configuration_focus_exited():
configuration.mono_path = _mono_line_edit.text
configuration.inklecate_path = _executable_line_edit.text
configuration.persist()
func _use_mono_toggled(_toggled: bool):
configuration.use_mono = !configuration.use_mono
configuration.persist()
_update_mono_availability(false)
func _mono_button_pressed():
_reset_file_dialog()
_file_dialog_selection = FileDialogSelection.MONO_EXECUTABLE
_file_dialog.current_path = configuration.mono_path
_file_dialog.current_dir = configuration.mono_path.get_base_dir()
_file_dialog.current_file = configuration.mono_path.get_file()
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_file_dialog.popup_centered(Vector2(1280, 800) * editor_interface.scale)
func _executable_button_pressed():
_reset_file_dialog()
_file_dialog_selection = FileDialogSelection.INKLECATE_EXECUTABLE
_file_dialog.current_file = configuration.inklecate_path
_file_dialog.current_dir = configuration.inklecate_path.get_base_dir()
_file_dialog.current_file = configuration.inklecate_path.get_file()
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_file_dialog.popup_centered(Vector2(1280, 800) * editor_interface.scale)
func _recompilation_mode_button_selected(index):
configuration.compilation_mode = index
configuration.persist()
func _test_button_pressed():
var test_configuration = InkExecutionConfiguration.new(configuration, false, true)
var tester = InkConfigurationTester.new(test_configuration)
var result = tester.test_availability()
# NOTE: At the moment, inklecate doesn't support a subcommand that would just
# exit with 0 so `_contains_inklecate_output_prefix` will always be executed.
if result.success:
var dialog = AcceptDialog.new()
add_child(dialog)
dialog.title = "Success"
dialog.dialog_text = "The configuration seems to be valid!"
dialog.popup_centered()
else:
var dialog = InkRichDialog.instantiate()
add_child(dialog)
dialog.title = "Error"
dialog.message_text = "Something went wrong while testing inklecate's setup. Please see the output below."
dialog.output_text = result.output
dialog.update_layout(editor_interface.scale)
dialog.popup_centered(Vector2(700, 400) * editor_interface.scale)
func _on_file_selected(path: String):
match _file_dialog_selection:
FileDialogSelection.MONO_EXECUTABLE:
configuration.mono_path = ProjectSettings.globalize_path(path)
_update_save_and_cleanup(configuration.mono_path, _mono_line_edit)
FileDialogSelection.INKLECATE_EXECUTABLE:
configuration.inklecate_path = ProjectSettings.globalize_path(path)
_update_save_and_cleanup(configuration.inklecate_path, _executable_line_edit)
_:
printerr("[inkgd] [ERROR] Unknown FileDialogSelection, failed to save FileDialog file.")
_file_dialog_selection = FileDialogSelection.UNKNOWN
func _check_runtime_presence():
var ink_engine_runtime = InkCSharpValidator.new().get_runtime_path()
var is_present = !ink_engine_runtime.is_empty()
if is_present:
_mono_support_presence_label.add_theme_color_override("font_color", Color.GREEN)
_mono_support_presence_label.text = "PRESENT"
else:
_mono_support_presence_label.add_theme_color_override("font_color", Color.RED)
_mono_support_presence_label.text = "MISSING"
func _mono_support_documentation_pressed():
OS.shell_open("https://inkgd.readthedocs.io/en/latest/advanced/migrating_to_godot_mono.html")
# ############################################################################ #
# Private helpers
# ############################################################################ #
func _reset_file_dialog():
_file_dialog.current_file = ""
_file_dialog.clear_filters()
func _update_save_and_cleanup(value, line_edit):
line_edit.text = value
line_edit.queue_redraw()
configuration.persist()
func _apply_configuration():
var compilation_mode = configuration.compilation_mode
var item_count = _recompilation_mode_button.get_item_count()
if compilation_mode >= 0 && compilation_mode < item_count:
_recompilation_mode_button.select(configuration.compilation_mode)
else:
_recompilation_mode_button.select(0)
_mono_line_edit.text = configuration.mono_path
_executable_line_edit.text = configuration.inklecate_path
_update_mono_availability(true)
func _update_mono_availability(updates_checkbox: bool = false):
var is_running_on_windows: bool = editor_interface.is_running_on_windows
var is_control_visible: bool = !is_running_on_windows && configuration.use_mono
_use_mono_label.visible = !is_running_on_windows
_use_mono_checkbox.visible = !is_running_on_windows
_mono_label.visible = is_control_visible
_mono_container.visible = is_control_visible
if updates_checkbox:
_use_mono_checkbox.set_pressed(configuration.use_mono)
func _set_button_icons():
var folder_icon = get_theme_icon("Folder", "EditorIcons")
var reload_icon = get_theme_icon("Reload", "EditorIcons")
var instance_icon = get_theme_icon("Instance", "EditorIcons")
_mono_dialog_button.icon = folder_icon
_executable_dialog_button.icon = folder_icon
_mono_support_documentation_button.icon = instance_icon
_mono_support_refresh_button.icon = reload_icon
func _connect_signals():
editor_interface.editor_filesystem.connect("filesystem_changed", Callable(self, "_check_runtime_presence"))
_test_button.connect("pressed", Callable(self, "_test_button_pressed"))
_use_mono_checkbox.connect("toggled", Callable(self, "_use_mono_toggled"))
_mono_line_edit.connect("text_submitted", Callable(self, "_configuration_entered"))
_executable_line_edit.connect("text_submitted", Callable(self, "_configuration_entered"))
_mono_line_edit.connect("focus_exited", Callable(self, "_configuration_focus_exited"))
_executable_line_edit.connect("focus_exited", Callable(self, "_configuration_focus_exited"))
_mono_dialog_button.connect("pressed", Callable(self, "_mono_button_pressed"))
_executable_dialog_button.connect("pressed", Callable(self, "_executable_button_pressed"))
_recompilation_mode_button.connect("item_selected", Callable(self, "_recompilation_mode_button_selected"))
_mono_support_documentation_button.connect("pressed", Callable(self, "_mono_support_documentation_pressed"))
_mono_support_refresh_button.connect("pressed", Callable(self, "_check_runtime_presence"))
_file_dialog.connect("file_selected", Callable(self, "_on_file_selected"))
func _can_run_mono():
return type_exists("_GodotSharp")

View file

@ -0,0 +1,258 @@
[gd_scene load_steps=6 format=3 uid="uid://cbuep470x6krw"]
[ext_resource type="Script" path="res://addons/inkgd/editor/panel/configuration/ink_configuration_panel.gd" id="1"]
[sub_resource type="Image" id="Image_h71nq"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="7"]
image = SubResource("Image_h71nq")
[sub_resource type="Image" id="Image_e0kbj"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="5"]
image = SubResource("Image_e0kbj")
[node name="Configuration" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1")
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
pivot_offset = Vector2(1346, -25)
[node name="V" type="VBoxContainer" parent="ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 25
[node name="InklecateV" type="VBoxContainer" parent="ScrollContainer/V"]
layout_mode = 2
size_flags_horizontal = 3
[node name="H" type="HBoxContainer" parent="ScrollContainer/V/InklecateV"]
layout_mode = 2
[node name="Label" type="Label" parent="ScrollContainer/V/InklecateV/H"]
layout_mode = 2
size_flags_horizontal = 3
text = "Inklecate"
[node name="TestButton" type="Button" parent="ScrollContainer/V/InklecateV/H"]
layout_mode = 2
mouse_filter = 1
text = "Test configuration"
[node name="M" type="MarginContainer" parent="ScrollContainer/V/InklecateV"]
layout_mode = 2
[node name="Panel" type="Panel" parent="ScrollContainer/V/InklecateV/M"]
self_modulate = Color(1, 1, 1, 0.686275)
layout_mode = 2
[node name="M" type="MarginContainer" parent="ScrollContainer/V/InklecateV/M"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="V" type="VBoxContainer" parent="ScrollContainer/V/InklecateV/M/M"]
layout_mode = 2
size_flags_horizontal = 3
[node name="G" type="GridContainer" parent="ScrollContainer/V/InklecateV/M/M/V"]
layout_mode = 2
size_flags_horizontal = 3
columns = 2
[node name="UseMonoLabel" type="Label" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
tooltip_text = "When toggled, run inklecate through Mono.
Only enable this setting is you want to use an older version of inklecate or you have a custom setup.
Modern versions of inklecate come bundled with a mono runtime on all platforms."
mouse_filter = 1
text = "Use Mono / .NET Core"
[node name="UseMonoCheckBox" type="CheckBox" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
text = "Yes"
[node name="MonoLabel" type="Label" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
tooltip_text = "The path to Mono."
mouse_filter = 1
text = "Mono / .NET Core Executable"
[node name="MonoH" type="HBoxContainer" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
[node name="MonoLineEdit" type="LineEdit" parent="ScrollContainer/V/InklecateV/M/M/V/G/MonoH"]
layout_mode = 2
size_flags_horizontal = 3
text = "/Library/Frameworks/Mono.framework/Versions/Current/Commands/mono"
[node name="MonoDialogButton" type="Button" parent="ScrollContainer/V/InklecateV/M/M/V/G/MonoH"]
layout_mode = 2
icon = SubResource("7")
[node name="ExecutableLabel" type="Label" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
tooltip_text = "The path to inklecate."
mouse_filter = 1
text = "Executable"
[node name="ExecutableH" type="HBoxContainer" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ExecutableLineEdit" type="LineEdit" parent="ScrollContainer/V/InklecateV/M/M/V/G/ExecutableH"]
layout_mode = 2
size_flags_horizontal = 3
text = "/opt/homebrew/bin/inklecate"
[node name="ExecutableDialogButton" type="Button" parent="ScrollContainer/V/InklecateV/M/M/V/G/ExecutableH"]
layout_mode = 2
icon = SubResource("7")
[node name="Recompilation Mode" type="Label" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
tooltip_text = "Define if/when your stories should be recompiled.
Manual: no automatic compilation.
During Build: every time the project is built/ran, the stories will be recompiled.
On change: as soon as an Ink resource is reimported by Godot, trigger a recompilation. A folder to watch can be defined in the \"Story\" tab."
mouse_filter = 1
text = "Recompilation Mode"
[node name="RecompilationModeOptionButton" type="OptionButton" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
layout_mode = 2
item_count = 3
selected = 0
popup/item_0/text = "Manual"
popup/item_0/id = 0
popup/item_1/text = "During Build"
popup/item_1/id = 1
popup/item_2/text = "On change (experimental)"
popup/item_2/id = 2
[node name="InkDirectoryLabel" type="Label" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
visible = false
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
tooltip_text = "The path to Mono."
mouse_filter = 1
text = "Mono"
[node name="InkDirectoryH" type="HBoxContainer" parent="ScrollContainer/V/InklecateV/M/M/V/G"]
visible = false
layout_mode = 2
size_flags_horizontal = 3
[node name="InkDirectoryLineEdit" type="LineEdit" parent="ScrollContainer/V/InklecateV/M/M/V/G/InkDirectoryH"]
layout_mode = 2
size_flags_horizontal = 3
[node name="InkDIrectoryDialogButton" type="Button" parent="ScrollContainer/V/InklecateV/M/M/V/G/InkDirectoryH"]
layout_mode = 2
icon = SubResource("5")
[node name="MonoSupportV" type="VBoxContainer" parent="ScrollContainer/V"]
layout_mode = 2
size_flags_horizontal = 3
[node name="H" type="HBoxContainer" parent="ScrollContainer/V/MonoSupportV"]
layout_mode = 2
[node name="Label" type="Label" parent="ScrollContainer/V/MonoSupportV/H"]
layout_mode = 2
size_flags_horizontal = 3
text = "Mono Support"
[node name="DocumentationButton" type="Button" parent="ScrollContainer/V/MonoSupportV/H"]
layout_mode = 2
mouse_filter = 1
theme_override_constants/h_separation = 8
text = "Documentation"
[node name="M" type="MarginContainer" parent="ScrollContainer/V/MonoSupportV"]
layout_mode = 2
[node name="Panel" type="Panel" parent="ScrollContainer/V/MonoSupportV/M"]
self_modulate = Color(1, 1, 1, 0.686275)
layout_mode = 2
[node name="M" type="MarginContainer" parent="ScrollContainer/V/MonoSupportV/M"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="V" type="VBoxContainer" parent="ScrollContainer/V/MonoSupportV/M/M"]
layout_mode = 2
size_flags_horizontal = 3
[node name="G" type="GridContainer" parent="ScrollContainer/V/MonoSupportV/M/M/V"]
layout_mode = 2
size_flags_horizontal = 3
columns = 2
[node name="DLLLabel" type="Label" parent="ScrollContainer/V/MonoSupportV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
tooltip_text = "Whether the ink runtime DLL (ink-engine-runtime.dll) can be found in the project or not."
mouse_filter = 1
text = "ink DLL"
[node name="DLLH" type="HBoxContainer" parent="ScrollContainer/V/MonoSupportV/M/M/V/G"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PresenceLabel" type="Label" parent="ScrollContainer/V/MonoSupportV/M/M/V/G/DLLH"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
mouse_filter = 1
theme_override_colors/font_color = Color(1, 0, 0, 1)
text = "MISSING"
[node name="RefreshButton" type="Button" parent="ScrollContainer/V/MonoSupportV/M/M/V/G/DLLH"]
layout_mode = 2
theme_override_constants/h_separation = 8
text = "Scan"
icon = SubResource("7")

View file

@ -0,0 +1,106 @@
@tool
# warning-ignore-all:return_value_discarded
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends Control
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkBottomPanel
# ############################################################################ #
# Imports
# ############################################################################ #
var InkStoryPanelScene = load("res://addons/inkgd/editor/panel/stories/ink_story_panel.tscn")
var InkPreviewPanelScene = load("res://addons/inkgd/editor/panel/preview/ink_preview_panel.tscn")
var InkConfigurationPanelScene = load("res://addons/inkgd/editor/panel/configuration/ink_configuration_panel.tscn")
# ############################################################################ #
# Properties
# ############################################################################ #
var editor_interface: InkEditorInterface = null
var configuration: InkConfiguration = null
# ############################################################################ #
# Private Properties
# ############################################################################ #
var _progress_texture: AnimatedTexture
# ############################################################################ #
# Hierarchy Nodes
# ############################################################################ #
@onready var _tab_container: TabContainer = $TabContainer
@onready var _beta_button: LinkButton = $MarginContainer/LinkButton
@onready var _story_panel = InkStoryPanelScene.instantiate()
@onready var _preview_panel = InkPreviewPanelScene.instantiate()
@onready var _configuration_panel = InkConfigurationPanelScene.instantiate()
# ############################################################################ #
# Overrides
# ############################################################################ #
func _ready():
# FIXME: This needs investigating.
# Sanity check. It seems the editor instantiates tools script on their
# own, probably to add them to its tree. In that case, they won't have
# their dependencies injected, so we're not doing anything.
if editor_interface == null || configuration == null:
print("[inkgd] [INFO] Ink Bottom Panel: dependencies not met, ignoring.")
return
_progress_texture = _create_progress_texture()
_story_panel.editor_interface = editor_interface
_story_panel.configuration = configuration
_story_panel.progress_texture = _progress_texture
_preview_panel.editor_interface = editor_interface
_preview_panel.configuration = configuration
_preview_panel.progress_texture = _progress_texture
_configuration_panel.editor_interface = editor_interface
_configuration_panel.configuration = configuration
_tab_container.add_child(_story_panel)
_tab_container.add_child(_preview_panel)
_tab_container.add_child(_configuration_panel)
_beta_button.connect("pressed", Callable(self, "_open_github_issues"))
_set_minimum_panel_size()
# ############################################################################ #
# Signals Receivers
# ############################################################################ #
func _open_github_issues():
OS.shell_open("https://github.com/ephread/inkgd/issues/new?assignees=&labels=&template=bug_report.md")
# ############################################################################ #
# Private helpers
# ############################################################################ #
func _create_progress_texture() -> AnimatedTexture:
var animated_texture = AnimatedTexture.new()
animated_texture.frames = 8
for index in range(8):
var texture = get_theme_icon(str("Progress", (index + 1)), "EditorIcons")
animated_texture.set_frame_texture(index, texture)
return animated_texture
func _set_minimum_panel_size():
# Adapting the minimum size of the panel to the scale of the editor.
custom_minimum_size = Vector2(900, 245) * editor_interface.scale

View file

@ -0,0 +1,51 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/inkgd/editor/panel/ink_bottom_panel.gd" type="Script" id=1]
[node name="Control" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TabContainer" type="TabContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
custom_minimum_size = Vector2( 0, 428.75 )
size_flags_vertical = 3
tab_alignment = 0
drag_to_rearrange_enabled = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="MarginContainer" type="MarginContainer" parent="."]
anchor_right = 1.0
offset_bottom = 24.0
clip_contents = true
mouse_filter = 2
size_flags_horizontal = 0
size_flags_vertical = 0
theme_override_constants/margin_right = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_left = 5
theme_override_constants/margin_bottom = 5
__meta__ = {
"_edit_use_anchors_": false
}
[node name="LinkButton" type="LinkButton" parent="MarginContainer"]
self_modulate = Color( 1, 1, 1, 0.647059 )
offset_left = 1887.0
offset_top = 5.0
offset_right = 1915.0
offset_bottom = 19.0
tooltip_text = "Open a new issue on Github to report a problem!"
size_flags_horizontal = 8
text = "beta"
underline = 1
__meta__ = {
"_edit_use_anchors_": false
}

View file

@ -0,0 +1,366 @@
@tool
# ############################################################################ #
# Copyright © 2018-2022 Paul Joannon
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends Control
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkPreviewPanel
# ############################################################################ #
# Imports
# ############################################################################ #
var InkPlayerFactory := preload("res://addons/inkgd/ink_player_factory.gd") as GDScript
# ############################################################################ #
# Enums
# ############################################################################ #
enum StoryOrigin {
CONFIGURATION,
FILE
}
# ############################################################################ #
# Constants
# ############################################################################ #
const NAME = "name"
const STORY_ORIGIN = "story_origin"
const FILE_PATH = "file_path"
# ############################################################################ #
# Public Properties
# ############################################################################ #
var editor_interface: InkEditorInterface
var configuration: InkConfiguration
var progress_texture: AnimatedTexture
# ############################################################################ #
# Private Properties
# ############################################################################ #
var _scrollbar_max_value = -1
var _current_story_index = -1
var _custom_stories: Array = []
var _available_stories: Array = []
var _file_dialog = EditorFileDialog.new()
var _ink_player = InkPlayerFactory.create()
# ############################################################################ #
# On Ready | Private Properties
# ############################################################################ #
@onready var _play_icon = get_theme_icon("Play", "EditorIcons")
# ############################################################################ #
# On Ready | Private Nodes
# ############################################################################
@onready var _command_strip = find_child("CommandStripHBoxContainer")
@onready var _pick_story_button = _command_strip.get_node("PickStoryOptionButton")
@onready var _load_story_button = _command_strip.get_node("LoadStoryButton")
@onready var _start_button = _command_strip.get_node("StartButton")
@onready var _stop_button = _command_strip.get_node("StopButton")
@onready var _clear_button = _command_strip.get_node("ClearButton")
@onready var _scroll_container = find_child("ScrollContainer")
@onready var _story_container = _scroll_container.get_node("MarginContainer/StoryVBoxContainer")
@onready var _choice_area_container = find_child("ChoicesAreaVBoxContainer")
@onready var _choices_container = _choice_area_container.get_node("ChoicesVBoxContainer")
# ############################################################################ #
# Overrides
# ############################################################################ #
func _ready():
# FIXME: This needs investigating.
# Sanity check. It seems the editor instantiates tools script on their
# own, probably to add them to its tree. In that case, they won't have
# their dependencies injected, so we're not doing anything.
if editor_interface == null || configuration == null || progress_texture == null:
print("[inkgd] [INFO] Ink Preview Tab: dependencies not met, ignoring.")
return
add_child(_ink_player)
_connect_signals()
_apply_configuration()
_update_story_picker()
var load_icon = get_theme_icon("Load", "EditorIcons")
var stop_icon = get_theme_icon("Stop", "EditorIcons")
var clear_icon = get_theme_icon("Clear", "EditorIcons")
_start_button.icon = _play_icon
_load_story_button.icon = load_icon
_stop_button.icon = stop_icon
_clear_button.icon = clear_icon
_stop_button.visible = false
_choice_area_container.custom_minimum_size = Vector2(200, 0) * editor_interface.scale
_choice_area_container.visible = false
_file_dialog.connect("file_selected", Callable(self, "_on_file_selected"))
add_child(_file_dialog)
# ############################################################################ #
# Signal Receivers
# ############################################################################ #
func _start_button_pressed():
var file_path = _get_current_story_file_path()
if file_path == null:
return
print("[inkgd] [INFO] Previewing %s" % file_path)
_clear_content()
_ink_player.destroy()
_ink_player.ink_file = load(file_path)
_start_button.icon = progress_texture
_disable_command_strip(true)
_ink_player.create_story()
func _stop_button_pressed():
_start_button.visible = true
_stop_button.visible = false
_choice_area_container.visible = false
_ink_player.destroy()
_clear_choices()
_clear_content()
func _story_loaded(successfully: bool):
_disable_command_strip(false)
_start_button.icon = _play_icon
if !successfully:
return
_start_button.visible = false
_stop_button.visible = true
_ink_player.allow_external_function_fallbacks = true
_continue_story()
func _pick_story_button_selected(index):
if _current_story_index != index:
_stop_button_pressed()
_current_story_index = index
func _load_story_button_pressed():
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_file_dialog.access = FileDialog.ACCESS_RESOURCES
_file_dialog.add_filter("*.json;Compiled Ink story")
_file_dialog.popup_centered(Vector2(1280, 800) * editor_interface.scale)
func _choice_button_pressed(index):
_clear_choices()
_ink_player.choose_choice_index(index)
_continue_story()
func _on_file_selected(path: String):
if _custom_stories.has(path):
return
for story_configuration in self.configuration.stories:
var target_file_path = self.configuration.get_target_file_path(story_configuration)
if target_file_path == path:
return
_custom_stories.append(path)
_apply_configuration()
_update_story_picker(path)
func _scrollbar_changed():
var max_value = _scroll_container.get_v_scroll_bar().max_value
if _scrollbar_max_value == max_value && _scrollbar_max_value != -1:
return
_scrollbar_max_value = max_value
_scroll_container.scroll_vertical = max_value
func _configuration_changed():
# Cleaning everything on configuration change may end up being frustrating.
# But for now, it's going to make everybody's life easier.
_stop_button_pressed()
_apply_configuration()
_update_story_picker()
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _apply_configuration():
_available_stories.clear()
_current_story_index = -1
_pick_story_button.selected = _current_story_index
var i = 0
for story_configuration in self.configuration.stories:
var target_file_path = self.configuration.get_target_file_path(story_configuration)
if target_file_path != null && !target_file_path.is_empty():
_available_stories.append({
NAME: "Story %d - %s" % [i + 1, target_file_path.get_file()],
FILE_PATH: target_file_path,
STORY_ORIGIN: StoryOrigin.CONFIGURATION
})
i += 1
var j = 0
for custom_story_path in _custom_stories:
if custom_story_path != null && !custom_story_path.is_empty():
_available_stories.append({
NAME: custom_story_path.get_file(),
FILE_PATH: ProjectSettings.localize_path(custom_story_path),
STORY_ORIGIN: StoryOrigin.FILE
})
j += 1
func _update_story_picker(selected_path = null):
_pick_story_button.clear()
var i = 0
for story in _available_stories:
_pick_story_button.add_item(story[NAME], i)
if selected_path != null && story[FILE_PATH] == selected_path:
_current_story_index = i
_pick_story_button.selected = _current_story_index
i += 1
if _available_stories.size() > 0:
if _current_story_index == -1:
_current_story_index = 0
_pick_story_button.selected = _current_story_index
_pick_story_button.visible = true
else:
_current_story_index = -1
_pick_story_button.selected = _current_story_index
_pick_story_button.visible = false
func _continue_story():
while _ink_player.can_continue:
var text = _ink_player.continue_story()
if text.right(text.length() - 1) == "\n":
text.erase(text.length() - 1, 1)
var text_label = Label.new()
text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
text_label.text = text
_story_container.add_child(text_label)
var tags = _ink_player.current_tags
if !tags.is_empty():
var tag_label = Label.new()
tag_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
tag_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
tag_label.text = "# " + ", ".join(PackedStringArray(tags))
tag_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.4))
_story_container.add_child(tag_label)
var separator = HSeparator.new()
_story_container.add_child(separator)
if _ink_player.current_choices.size() > 0:
var i = 0
for choice in _ink_player.current_choices:
var button = Button.new()
button.text = choice.text
button.connect("pressed", Callable(self, "_choice_button_pressed").bind(i))
_choices_container.add_child(button)
i += 1
_choice_area_container.visible = true
else:
var label = Label.new()
label.text = "End of the story."
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_story_container.add_child(label)
_choice_area_container.visible = false
func _get_current_story_file_path():
if _current_story_index >= 0 && _current_story_index < _available_stories.size():
return _available_stories[_current_story_index][FILE_PATH]
else:
return null
func _clear_content():
for child in _story_container.get_children():
_story_container.remove_child(child)
child.queue_free()
func _clear_choices():
for child in _choices_container.get_children():
_choices_container.remove_child(child)
child.queue_free()
func _disable_command_strip(disabled: bool):
_pick_story_button.disabled = disabled
_load_story_button.disabled = disabled
_start_button.disabled = disabled
_stop_button.disabled = disabled
_clear_button.disabled = disabled
func _connect_signals():
if configuration != null:
var is_signal_connected := configuration.is_connected(
"story_configuration_changed",
Callable(self, "_configuration_changed")
)
if !is_signal_connected:
configuration.connect(
"story_configuration_changed",
Callable(self, "_configuration_changed")
)
_ink_player.connect("loaded", Callable(self, "_story_loaded"))
_pick_story_button.connect("item_selected", Callable(self, "_pick_story_button_selected"))
_load_story_button.connect("pressed", Callable(self, "_load_story_button_pressed"))
_start_button.connect("pressed", Callable(self, "_start_button_pressed"))
_stop_button.connect("pressed", Callable(self, "_stop_button_pressed"))
_clear_button.connect("pressed", Callable(self, "_clear_content"))
_scroll_container.get_v_scroll_bar().connect("changed", Callable(self, "_scrollbar_changed"))

View file

@ -0,0 +1,119 @@
[gd_scene load_steps=4 format=3 uid="uid://0do8xgmkscjn"]
[ext_resource type="Script" path="res://addons/inkgd/editor/panel/preview/ink_preview_panel.gd" id="2"]
[sub_resource type="Image" id="Image_nl482"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="2"]
image = SubResource("Image_nl482")
[node name="Preview" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("2")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HSplitContainer" type="HSplitContainer" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="StoryVBoxContainer" type="VBoxContainer" parent="MarginContainer/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 5
[node name="CommandStripHBoxContainer" type="HBoxContainer" parent="MarginContainer/HSplitContainer/StoryVBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 10
[node name="LoadStoryButton" type="Button" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/CommandStripHBoxContainer"]
layout_mode = 2
theme_override_constants/h_separation = 8
text = "Load new…"
icon = SubResource("2")
[node name="PickStoryOptionButton" type="OptionButton" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/CommandStripHBoxContainer"]
layout_mode = 2
size_flags_horizontal = 0
[node name="Control" type="Control" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/CommandStripHBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="StartButton" type="Button" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/CommandStripHBoxContainer"]
layout_mode = 2
theme_override_constants/h_separation = 8
text = "Start"
icon = SubResource("2")
[node name="StopButton" type="Button" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/CommandStripHBoxContainer"]
visible = false
layout_mode = 2
theme_override_constants/h_separation = 10
text = "Stop"
icon = SubResource("2")
[node name="ClearButton" type="Button" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/CommandStripHBoxContainer"]
layout_mode = 2
theme_override_constants/h_separation = 8
text = "Clear"
icon = SubResource("2")
[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/HSplitContainer/StoryVBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Panel" type="Panel" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/MarginContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="StoryVBoxContainer" type="VBoxContainer" parent="MarginContainer/HSplitContainer/StoryVBoxContainer/MarginContainer/ScrollContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ChoicesAreaVBoxContainer" type="VBoxContainer" parent="MarginContainer/HSplitContainer"]
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
theme_override_constants/separation = 5
[node name="Button" type="Button" parent="MarginContainer/HSplitContainer/ChoicesAreaVBoxContainer"]
layout_mode = 2
mouse_filter = 2
text = "Choices"
flat = true
[node name="ChoicesVBoxContainer" type="VBoxContainer" parent="MarginContainer/HSplitContainer/ChoicesAreaVBoxContainer"]
layout_mode = 2

View file

@ -0,0 +1,13 @@
[gd_scene format=2]
[node name="EmptyStateContainer" type="CenterContainer"]
offset_right = 1902.0
offset_bottom = 100.0
custom_minimum_size = Vector2( 0, 100 )
[node name="Label" type="Label" parent="."]
offset_left = 879.0
offset_top = 43.0
offset_right = 1022.0
offset_bottom = 57.0
text = "No stories to compile."

View file

@ -0,0 +1,153 @@
@tool
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends VBoxContainer
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkStoryConfiguration
# ############################################################################ #
# Signals
# ############################################################################ #
signal configuration_changed(story_configuration)
signal remove_button_pressed(story_configuration)
signal build_button_pressed(story_configuration)
signal source_file_button_pressed(story_configuration)
signal target_file_button_pressed(story_configuration)
signal watched_folder_button_pressed(story_configuration)
# ############################################################################ #
# Properties
# ############################################################################ #
var editor_interface: InkEditorInterface = null
# ############################################################################ #
# Nodes
# ############################################################################ #
@onready var story_label = find_child("StoryLabel")
@onready var remove_button = find_child("RemoveButton")
@onready var build_button = find_child("BuildButton")
@onready var source_file_line_edit = find_child("SourceFileLineEdit")
@onready var source_file_dialog_button = find_child("SourceFileDialogButton")
@onready var target_file_line_edit = find_child("TargetFileLineEdit")
@onready var target_file_dialog_button = find_child("TargetFileDialogButton")
@onready var watched_folder_label = find_child("WatchedFolderLabel")
@onready var watched_folder_container = find_child("WatchedFolderHBoxContainer")
@onready var watched_folder_line_edit = find_child("WatchedFolderLineEdit")
@onready var watched_folder_dialog_button = find_child("WatchedFolderDialogButton")
@onready var background_color_rect = find_child("BackgroundColorRect")
# ############################################################################ #
# Overrides
# ############################################################################ #
func _ready():
# FIXME: This needs investigating.
# Sanity check. It seems the editor instantiates tools script on their
# own, probably to add them to its tree. In that case, they won't have
# their dependencies injected, so we're not doing anything.
if editor_interface == null:
return
_apply_custom_header_color()
_set_button_icons()
_connect_signals()
show_watched_folder(false)
# ############################################################################ #
# Signals
# ############################################################################ #
func _configuration_entered(_new_text: String):
_configuration_focus_exited()
func _configuration_focus_exited():
emit_signal("configuration_changed", self)
func _remove_button_pressed():
emit_signal("remove_button_pressed", self)
func _build_button_pressed():
emit_signal("build_button_pressed", self)
func _source_file_button_pressed():
emit_signal("source_file_button_pressed", self)
func _target_file_button_pressed():
emit_signal("target_file_button_pressed", self)
func _watched_folder_button_pressed():
emit_signal("watched_folder_button_pressed", self)
# ############################################################################ #
# Public Methods
# ############################################################################ #
@warning_ignore("shadowed_variable")
func show_watched_folder(show: bool):
watched_folder_label.visible = show
watched_folder_container.visible = show
func disable_all_buttons(disable: bool):
remove_button.disabled = disable
build_button.disabled = disable
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _apply_custom_header_color():
var header_color = editor_interface.get_custom_header_color()
if header_color != Color.TRANSPARENT:
background_color_rect.color = header_color
func _set_button_icons():
var folder_icon = get_theme_icon("Folder", "EditorIcons")
source_file_dialog_button.icon = folder_icon
target_file_dialog_button.icon = folder_icon
watched_folder_dialog_button.icon = folder_icon
var trash_icon = get_theme_icon("Remove", "EditorIcons")
remove_button.icon = trash_icon
func _connect_signals():
source_file_line_edit.connect("text_submitted", Callable(self, "_configuration_entered"))
source_file_line_edit.connect("focus_exited", Callable(self, "_configuration_focus_exited"))
target_file_line_edit.connect("text_submitted", Callable(self, "_configuration_entered"))
target_file_line_edit.connect("focus_exited", Callable(self, "_configuration_focus_exited"))
source_file_dialog_button.connect("pressed", Callable(self, "_source_file_button_pressed"))
target_file_dialog_button.connect("pressed", Callable(self, "_target_file_button_pressed"))
watched_folder_dialog_button.connect("pressed", Callable(self, "_watched_folder_button_pressed"))
remove_button.connect("pressed", Callable(self, "_remove_button_pressed"))
build_button.connect("pressed", Callable(self, "_build_button_pressed"))

View file

@ -0,0 +1,189 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://addons/inkgd/editor/panel/stories/ink_story_configuration.gd" type="Script" id=1]
[sub_resource type="Image" id=3]
data = {
"data": PackedByteArray( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id=2]
flags = 4
flags = 4
image = SubResource( 3 )
size = Vector2( 16, 16 )
[node name="StoryConfiguration" type="VBoxContainer"]
offset_right = 1902.0
offset_bottom = 87.0
theme_override_constants/separation = 5
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="HeaderMarginContainer" type="MarginContainer" parent="."]
offset_right = 1902.0
offset_bottom = 32.0
theme_override_constants/margin_right = 0
theme_override_constants/margin_top = 0
theme_override_constants/margin_left = 0
theme_override_constants/margin_bottom = 0
[node name="BackgroundColorRect" type="ColorRect" parent="HeaderMarginContainer"]
offset_right = 1902.0
offset_bottom = 32.0
color = Color( 0.268314, 0.291712, 0.340784, 1 )
[node name="MarginContainer" type="MarginContainer" parent="HeaderMarginContainer"]
offset_right = 1902.0
offset_bottom = 32.0
theme_override_constants/margin_right = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_left = 25
theme_override_constants/margin_bottom = 5
[node name="HBoxContainer" type="HBoxContainer" parent="HeaderMarginContainer/MarginContainer"]
offset_left = 25.0
offset_top = 5.0
offset_right = 1897.0
offset_bottom = 27.0
[node name="StoryLabel" type="Label" parent="HeaderMarginContainer/MarginContainer/HBoxContainer"]
offset_top = 4.0
offset_right = 1711.0
offset_bottom = 18.0
size_flags_horizontal = 3
text = "0"
[node name="RemoveButton" type="Button" parent="HeaderMarginContainer/MarginContainer/HBoxContainer"]
offset_left = 1715.0
offset_right = 1803.0
offset_bottom = 22.0
mouse_filter = 1
theme_override_constants/h_separation = 8
text = "Remove"
icon = SubResource( 2 )
__meta__ = {
"_edit_use_anchors_": false,
"_editor_description_": "Compile the story manually, based on the configuration below."
}
[node name="BuildButton" type="Button" parent="HeaderMarginContainer/MarginContainer/HBoxContainer"]
offset_left = 1807.0
offset_right = 1872.0
offset_bottom = 22.0
mouse_filter = 1
theme_override_constants/h_separation = 8
text = "Compile"
__meta__ = {
"_edit_use_anchors_": false,
"_editor_description_": "Compile the story manually, based on the configuration below."
}
[node name="ConfigurationMarginContainer" type="MarginContainer" parent="."]
offset_top = 37.0
offset_right = 1902.0
offset_bottom = 117.0
theme_override_constants/margin_right = 0
theme_override_constants/margin_top = 0
theme_override_constants/margin_left = 5
theme_override_constants/margin_bottom = 0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="GridContainer" type="GridContainer" parent="ConfigurationMarginContainer"]
offset_left = 5.0
offset_right = 1902.0
offset_bottom = 80.0
size_flags_horizontal = 3
columns = 2
[node name="SourceFileLabel" type="Label" parent="ConfigurationMarginContainer/GridContainer"]
offset_right = 946.0
offset_bottom = 24.0
tooltip_text = "The input path, a.k.a the original ink story."
mouse_filter = 1
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Source File (*.ink)"
[node name="SourceFileHBoxContainer" type="HBoxContainer" parent="ConfigurationMarginContainer/GridContainer"]
offset_left = 950.0
offset_right = 1896.0
offset_bottom = 24.0
size_flags_horizontal = 3
[node name="SourceFileLineEdit" type="LineEdit" parent="ConfigurationMarginContainer/GridContainer/SourceFileHBoxContainer"]
offset_right = 914.0
offset_bottom = 24.0
size_flags_horizontal = 3
[node name="SourceFileDialogButton" type="Button" parent="ConfigurationMarginContainer/GridContainer/SourceFileHBoxContainer"]
offset_left = 918.0
offset_right = 946.0
offset_bottom = 24.0
icon = SubResource( 2 )
[node name="TargetFileLabel" type="Label" parent="ConfigurationMarginContainer/GridContainer"]
offset_top = 28.0
offset_right = 946.0
offset_bottom = 52.0
tooltip_text = "The output path, a.k.a the JSON file compiled from the original ink story."
mouse_filter = 1
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Target File (*.json)"
[node name="TargetFileHBoxContainer" type="HBoxContainer" parent="ConfigurationMarginContainer/GridContainer"]
offset_left = 950.0
offset_top = 28.0
offset_right = 1896.0
offset_bottom = 52.0
size_flags_horizontal = 3
[node name="TargetFileLineEdit" type="LineEdit" parent="ConfigurationMarginContainer/GridContainer/TargetFileHBoxContainer"]
offset_right = 914.0
offset_bottom = 24.0
size_flags_horizontal = 3
[node name="TargetFileDialogButton" type="Button" parent="ConfigurationMarginContainer/GridContainer/TargetFileHBoxContainer"]
offset_left = 918.0
offset_right = 946.0
offset_bottom = 24.0
icon = SubResource( 2 )
[node name="WatchedFolderLabel" type="Label" parent="ConfigurationMarginContainer/GridContainer"]
offset_top = 56.0
offset_right = 946.0
offset_bottom = 80.0
tooltip_text = "The directory to watch for changes in Ink sources.
This path is optional, leave it blank to disable automatic recompilation for that story."
mouse_filter = 1
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Watched Folder"
[node name="WatchedFolderHBoxContainer" type="HBoxContainer" parent="ConfigurationMarginContainer/GridContainer"]
offset_left = 950.0
offset_top = 56.0
offset_right = 1896.0
offset_bottom = 80.0
size_flags_horizontal = 3
[node name="WatchedFolderLineEdit" type="LineEdit" parent="ConfigurationMarginContainer/GridContainer/WatchedFolderHBoxContainer"]
offset_right = 914.0
offset_bottom = 24.0
size_flags_horizontal = 3
[node name="WatchedFolderDialogButton" type="Button" parent="ConfigurationMarginContainer/GridContainer/WatchedFolderHBoxContainer"]
offset_left = 918.0
offset_right = 946.0
offset_bottom = 24.0
icon = SubResource( 2 )

View file

@ -0,0 +1,524 @@
@tool
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# Licensed under the MIT License.
# See LICENSE in the project root for license information.
# ############################################################################ #
extends Control
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkStoryPanel
# ############################################################################ #
# Imports
# ############################################################################ #
var InkConfiguration = load("res://addons/inkgd/editor/common/ink_configuration.gd")
var InkCompilationConfiguration = load("res://addons/inkgd/editor/common/executors/structures/ink_compilation_configuration.gd")
var InkCompiler = load("res://addons/inkgd/editor/common/executors/ink_compiler.gd")
var InkRichDialog = load("res://addons/inkgd/editor/panel/common/ink_rich_dialog.tscn")
var InkProgressDialog = load("res://addons/inkgd/editor/panel/common/ink_progress_dialog.tscn")
var InkStoryConfigurationScene = load("res://addons/inkgd/editor/panel/stories/ink_story_configuration.tscn")
var EmptyStateContainerScene = load("res://addons/inkgd/editor/panel/stories/empty_state_container.tscn")
# ############################################################################ #
# Signals
# ############################################################################ #
signal _compiled()
# ############################################################################ #
# Enums
# ############################################################################ #
enum FileDialogSelection {
UNKNOWN,
SOURCE_FILE,
TARGET_FILE,
WATCHED_FOLDER
}
# ############################################################################ #
# Properties
# ############################################################################ #
var editor_interface: InkEditorInterface
var configuration: InkConfiguration
var progress_texture: AnimatedTexture
# ############################################################################ #
# Private Properties
# ############################################################################ #
var _scrollbar_max_value = -1
var _compilers: Dictionary = {}
var _file_dialog = EditorFileDialog.new()
## Configuration item for which the FileDialog is currently shown.
##
## Unknown by default.
var _file_dialog_selection = FileDialogSelection.UNKNOWN
## The story index for which the FileDialog is currenlty shown.
##
## -1 by default or when the file dialog currently displayed doesn't
## concern the stories source/target files.
var _file_dialog_selection_story_index = -1
var _current_story_node = null
var _progress_dialog = null
# ############################################################################ #
# Nodes
# ############################################################################ #
@onready var _empty_state_container = EmptyStateContainerScene.instantiate()
@onready var _build_all_button = find_child("BuildAllButton")
@onready var _add_new_story_button = find_child("AddNewStoryButton")
@onready var _story_configuration_container = find_child("StoryConfigurationVBoxContainer")
@onready var _scroll_container = find_child("ScrollContainer")
# ############################################################################ #
# Overrides
# ############################################################################ #
func _ready():
# FIXME: This needs investigating.
# Sanity check. It seems the editor instantiates tools script on their
# own, probably to add them to its tree. In that case, they won't have
# their dependencies injected, so we're not doing anything.
if editor_interface == null || configuration == null || progress_texture == null:
print("[inkgd] [INFO] Ink Stories Tab: dependencies not met, ignoring.")
return
configuration.connect("compilation_mode_changed", Callable(self, "_compilation_mode_changed"))
editor_interface.editor_filesystem.connect("resources_reimported", Callable(self, "_resources_reimported"))
_story_configuration_container.add_child(_empty_state_container)
add_child(_file_dialog)
var add_icon = get_theme_icon("Add", "EditorIcons")
_add_new_story_button.icon = add_icon
_load_story_configurations()
_connect_signals()
_compilation_mode_changed(configuration.compilation_mode)
# ############################################################################ #
# Signal Receivers
# ############################################################################ #
func _resources_reimported(resources):
call_deferred("_recompile_if_necessary", resources)
func _compilation_mode_changed(compilation_mode: int):
var show_folder = (compilation_mode == InkConfiguration.BuildMode.AFTER_CHANGE)
for child in _story_configuration_container.get_children():
child.show_watched_folder(show_folder)
func _source_file_button_pressed(node):
_reset_file_dialog()
var index = _get_story_configuration_index(node)
_file_dialog_selection = FileDialogSelection.SOURCE_FILE
_file_dialog_selection_story_index = index
var story_configuration = _get_story_configuration_at_index(index)
var path = story_configuration.source_file_line_edit.text
_file_dialog.current_path = path
_file_dialog.current_dir = path.get_base_dir()
_file_dialog.current_file = path.get_file()
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_file_dialog.add_filter("*.ink;Ink source file")
_file_dialog.popup_centered(Vector2(1280, 800) * editor_interface.scale)
func _target_file_button_pressed(node):
_reset_file_dialog()
var index = _get_story_configuration_index(node)
_file_dialog_selection = FileDialogSelection.TARGET_FILE
_file_dialog_selection_story_index = index
var story_configuration = _get_story_configuration_at_index(index)
var path = story_configuration.target_file_line_edit.text
_file_dialog.current_path = path
_file_dialog.current_dir = path.get_base_dir()
_file_dialog.current_file = path.get_file()
_file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_file_dialog.add_filter("*.json;Compiled Ink story")
_file_dialog.popup_centered(Vector2(1280, 800) * editor_interface.scale)
func _watched_folder_button_pressed(node):
_reset_file_dialog()
var index = _get_story_configuration_index(node)
_file_dialog_selection = FileDialogSelection.WATCHED_FOLDER
_file_dialog_selection_story_index = index
var story_configuration = _get_story_configuration_at_index(index)
var path = story_configuration.watched_folder_line_edit.text
_file_dialog.current_path = path
_file_dialog.current_dir = path.get_base_dir()
_file_dialog.current_file = path.get_file()
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_file_dialog.popup_centered(Vector2(1280, 800) * editor_interface.scale)
func _build_all_button_pressed():
_compile_all_stories()
func _add_new_story_button_pressed():
_add_new_story_configuration()
func _configuration_changed(_node):
_persist_configuration()
func _remove_button_pressed(node):
var index = _get_story_configuration_index(node)
configuration.remove_story_configuration_at_index(index)
# TODO: Rebuild from scratch instead.
var parent = node.get_parent()
if parent != null:
parent.remove_child(node)
node.queue_free()
if _story_configuration_container.get_child_count() == 0:
_story_configuration_container.add_child(_empty_state_container)
else:
var i = 0
for child in _story_configuration_container.get_children():
# Not using "is InkStoryConfiguration", because it requires a type
# declaration. Node Types register in the editor and we don't want
# that. This is a bit hacky, but until the proposal is accepted,
# it prevents cluttering the "Create new node" list.
if "story_label" in child:
child.story_label.text = "Story %d" % (i + 1)
i += 1
_persist_configuration()
func _build_button_pressed(node):
var index = _get_story_configuration_index(node)
var story_configuration = configuration.get_story_configuration_at_index(index)
if story_configuration == null:
printerr("[inkgd] [ERROR] No configurations found for Story %d" % (index + 1))
return
_compile_story(story_configuration, node)
func _compile_all_stories():
_disable_all_buttons(true)
var number_of_stories = configuration.stories.size()
var current_story_index = 0
_progress_dialog = InkProgressDialog.instantiate()
add_child(_progress_dialog)
_progress_dialog.update_layout(editor_interface.scale)
_progress_dialog.popup_centered(Vector2(600, 100) * editor_interface.scale)
for story_configuration in configuration.stories:
var source_file_path: String = configuration.get_source_file_path(story_configuration)
_progress_dialog.current_step_name = source_file_path.get_file()
_compile_story(story_configuration)
await self._compiled
@warning_ignore("integer_division")
_progress_dialog.progress = float(100 * (current_story_index + 1) / number_of_stories)
current_story_index += 1
remove_child(_progress_dialog)
_progress_dialog.queue_free()
_progress_dialog = null
_disable_all_buttons(false)
func _compile_story(story_configuration, node = null):
var source_file_path = configuration.get_source_file_path(story_configuration)
var target_file_path = configuration.get_target_file_path(story_configuration)
if node != null:
_current_story_node = node
node.build_button.icon = progress_texture
_disable_all_buttons(true)
var compiler_configuration = InkCompilationConfiguration.new(
configuration,
true,
node != null,
source_file_path,
target_file_path
)
var compiler = InkCompiler.new(compiler_configuration)
_compilers[compiler.identifier] = compiler
compiler.connect("story_compiled", Callable(self, "_handle_compilation"))
compiler.compile_story()
func _handle_compilation(result):
if _current_story_node != null:
var button = _current_story_node.build_button
button.icon = null
_disable_all_buttons(false)
_current_story_node = null
if result.user_triggered:
if result.success:
if result.output && !result.output.is_empty():
var dialog = InkRichDialog.instantiate()
add_child(dialog)
dialog.title = "Success!"
dialog.message_text = "The story was successfully compiled."
dialog.output_text = result.output
dialog.update_layout(editor_interface.scale)
dialog.popup_centered(Vector2(700, 400) * editor_interface.scale)
else:
var dialog = AcceptDialog.new()
add_child(dialog)
dialog.title = "Success!"
dialog.dialog_text = "The story was successfully compiled."
dialog.popup_centered()
_reimport_compiled_stories()
else:
var dialog = InkRichDialog.instantiate()
add_child(dialog)
dialog.title = "Error"
dialog.message_text = "The story could not be compiled. See inklecate's output below."
dialog.output_text = result.output
dialog.update_layout(editor_interface.scale)
dialog.popup_centered(Vector2(700, 400) * editor_interface.scale)
else:
_reimport_compiled_stories()
if _compilers.has(result.identifier):
_compilers.erase(result.identifier)
emit_signal("_compiled")
func _on_file_selected(path: String):
var index = _file_dialog_selection_story_index
match _file_dialog_selection:
FileDialogSelection.SOURCE_FILE:
var story_configuration = _get_story_configuration_at_index(index)
if story_configuration == null:
return
var localized_path = ProjectSettings.localize_path(path)
var source_line_edit = story_configuration.source_file_line_edit
source_line_edit.text = localized_path
source_line_edit.queue_redraw()
if story_configuration.target_file_line_edit.text.is_empty():
var target_line_edit = story_configuration.target_file_line_edit
target_line_edit.text = localized_path + ".json"
target_line_edit.queue_redraw()
if story_configuration.watched_folder_line_edit.text.is_empty():
var watched_folder_line_edit = story_configuration.watched_folder_line_edit
watched_folder_line_edit.text = localized_path.get_base_dir()
watched_folder_line_edit.queue_redraw()
_persist_configuration()
FileDialogSelection.TARGET_FILE:
var story_configuration = _get_story_configuration_at_index(index)
if story_configuration == null:
return
var localized_path = ProjectSettings.localize_path(path)
var line_edit = story_configuration.target_file_line_edit
line_edit.text = localized_path
line_edit.queue_redraw()
_persist_configuration()
FileDialogSelection.WATCHED_FOLDER:
var story_configuration = _get_story_configuration_at_index(index)
if story_configuration == null:
return
var localized_path = ProjectSettings.localize_path(path)
var line_edit = story_configuration.watched_folder_line_edit
line_edit.text = localized_path
line_edit.queue_redraw()
_persist_configuration()
_:
printerr("[inkgd] [ERROR] Unknown FileDialogSelection, failed to save FileDialog file.")
_file_dialog_selection = FileDialogSelection.UNKNOWN
func _scrollbar_changed():
var max_value = _scroll_container.get_v_scroll_bar().max_value
if _scrollbar_max_value == max_value && _scrollbar_max_value != -1:
return
_scrollbar_max_value = max_value
_scroll_container.scroll_vertical = max_value
# ############################################################################ #
# Private helpers
# ############################################################################ #
func _reset_file_dialog():
_file_dialog.current_path = "res://"
_file_dialog.current_dir = "res://"
_file_dialog.current_file = ""
_file_dialog.clear_filters()
func _persist_configuration():
configuration.stories.clear()
if _empty_state_container.get_parent() == null:
configuration.stories.clear()
for node in _story_configuration_container.get_children():
# Not using "is InkStoryConfiguration", because it requires a type
# declaration. Node Types register in the editor and we don't want
# that. This is a bit hacky, but until the proposal is accepted,
# it prevents cluttering the "Create new node" list.
if !("story_label" in node):
continue
configuration.append_new_story_configuration(
node.source_file_line_edit.text,
node.target_file_line_edit.text,
node.watched_folder_line_edit.text
)
configuration.persist()
func _load_story_configurations():
for story_configuration in configuration.stories:
var node = _add_new_story_configuration()
node.source_file_line_edit.text = configuration.get_source_file_path(story_configuration)
node.target_file_line_edit.text = configuration.get_target_file_path(story_configuration)
node.watched_folder_line_edit.text = configuration.get_watched_folder_path(story_configuration)
func _add_new_story_configuration():
var story_configuration = InkStoryConfigurationScene.instantiate()
story_configuration.editor_interface = editor_interface
story_configuration.connect("configuration_changed", Callable(self, "_configuration_changed"))
story_configuration.connect("remove_button_pressed", Callable(self, "_remove_button_pressed"))
story_configuration.connect("build_button_pressed", Callable(self, "_build_button_pressed"))
story_configuration.connect("source_file_button_pressed", Callable(self, "_source_file_button_pressed"))
story_configuration.connect("target_file_button_pressed", Callable(self, "_target_file_button_pressed"))
story_configuration.connect("watched_folder_button_pressed", Callable(self, "_watched_folder_button_pressed"))
if _empty_state_container.get_parent() != null:
_story_configuration_container.remove_child(_empty_state_container)
_story_configuration_container.add_child(story_configuration)
var count = _story_configuration_container.get_child_count()
story_configuration.story_label.text = "Story %d" % count
var show_folder = (configuration.compilation_mode == InkConfiguration.BuildMode.AFTER_CHANGE)
story_configuration.show_watched_folder(show_folder)
return story_configuration
func _reimport_compiled_stories():
editor_interface.scan_file_system()
func _get_story_configuration_index(node) -> int:
return _story_configuration_container.get_children().find(node)
func _get_story_configuration_at_index(index: int):
if index >= 0 && _story_configuration_container.get_child_count():
return _story_configuration_container.get_children()[index]
return null
func _recompile_if_necessary(resources: PackedStringArray):
# Making sure the resources have been imported before recompiling.
await get_tree().create_timer(0.5).timeout
for story_configuration in configuration.stories:
var watched_folder_path: String = configuration.get_watched_folder_path(story_configuration)
if watched_folder_path.is_empty():
return
for resource in resources:
if resource.begins_with(watched_folder_path):
_compile_story(story_configuration)
break
func _disable_all_buttons(disable: bool):
_add_new_story_button.disabled = disable
_build_all_button.disabled = disable
for child in _story_configuration_container.get_children():
child.disable_all_buttons(disable)
func _connect_signals():
_build_all_button.connect("pressed", Callable(self, "_build_all_button_pressed"))
_add_new_story_button.connect("pressed", Callable(self, "_add_new_story_button_pressed"))
_file_dialog.connect("file_selected", Callable(self, "_on_file_selected"))
_file_dialog.connect("dir_selected", Callable(self, "_on_file_selected"))
_scroll_container.get_v_scroll_bar().connect("changed", Callable(self, "_scrollbar_changed"))

View file

@ -0,0 +1,75 @@
[gd_scene load_steps=4 format=3 uid="uid://b16uj28y2mqmk"]
[ext_resource type="Script" path="res://addons/inkgd/editor/panel/stories/ink_story_panel.gd" id="1"]
[sub_resource type="Image" id="Image_3omov"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="2"]
image = SubResource("Image_3omov")
[node name="Story" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1")
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="StoriesVBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="ScrollContainer/StoriesVBoxContainer"]
layout_mode = 2
alignment = 2
[node name="Label" type="Label" parent="ScrollContainer/StoriesVBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Managed Stories"
[node name="AddNewStoryButton" type="Button" parent="ScrollContainer/StoriesVBoxContainer/HBoxContainer"]
layout_mode = 2
mouse_filter = 1
theme_override_constants/h_separation = 8
text = "New story"
icon = SubResource("2")
[node name="VSeparator" type="VSeparator" parent="ScrollContainer/StoriesVBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="BuildAllButton" type="Button" parent="ScrollContainer/StoriesVBoxContainer/HBoxContainer"]
layout_mode = 2
mouse_filter = 1
theme_override_constants/h_separation = 10
text = "Compile All"
[node name="MarginContainer" type="MarginContainer" parent="ScrollContainer/StoriesVBoxContainer"]
layout_mode = 2
[node name="Panel" type="Panel" parent="ScrollContainer/StoriesVBoxContainer/MarginContainer"]
self_modulate = Color(1, 1, 1, 0.686275)
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="ScrollContainer/StoriesVBoxContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="StoryConfigurationVBoxContainer" type="VBoxContainer" parent="ScrollContainer/StoriesVBoxContainer/MarginContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 15

View file

View file

@ -0,0 +1,101 @@
# warning-ignore-all:return_value_discarded
extends %BASE%
# ############################################################################ #
# Imports
# ############################################################################ #
var InkPlayer = load("res://addons/inkgd/ink_player.gd")
# ############################################################################ #
# Public Nodes
# ############################################################################ #
# Alternatively, it could also be retrieved from the tree.
# onready var _ink_player = $InkPlayer
@onready var _ink_player = InkPlayer.new()
# ############################################################################ #
# Lifecycle
# ############################################################################ #
func _ready():
%TS%# Adds the player to the tree.
%TS%add_child(_ink_player)
%TS%# Replace the example path with the path to your story.
%TS%# Remove this line if you set 'ink_file' in the inspector.
%TS%_ink_player.ink_file = load("res://path/to/file.ink.json")
%TS%# It's recommended to load the story in the background. On platforms that
%TS%# don't support threads, the value of this variable is ignored.
%TS%_ink_player.loads_in_background = true
%TS%_ink_player.connect("loaded", Callable(self, "_story_loaded"))
%TS%# Creates the story. 'loaded' will be emitted once Ink is ready
%TS%# continue the story.
%TS%_ink_player.create_story()
# ############################################################################ #
# Signal Receivers
# ############################################################################ #
func _story_loaded(successfully: bool):
%TS%if !successfully:
%TS%%TS%return
%TS%# _observe_variables()
%TS%# _bind_externals()
%TS%_continue_story()
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _continue_story():
%TS%while _ink_player.can_continue:
%TS%%TS%var text = _ink_player.continue_story()
%TS%%TS%# This text is a line of text from the ink story.
%TS%%TS%# Set the text of a Label to this value to display it in your game.
%TS%%TS%print(text)
%TS%if _ink_player.has_choices:
%TS%%TS%# 'current_choices' contains a list of the choices, as strings.
%TS%%TS%for choice in _ink_player.current_choices:
%TS%%TS%%TS%print(choice.text)
%TS%%TS%%TS%print(choice.tags)
%TS%%TS%# '_select_choice' is a function that will take the index of
%TS%%TS%# your selection and continue the story.
%TS%%TS%_select_choice(0)
%TS%else:
%TS%%TS%# This code runs when the story reaches it's end.
%TS%%TS%print("The End")
func _select_choice(index):
%TS%_ink_player.choose_choice_index(index)
%TS%_continue_story()
# Uncomment to bind an external function.
#
# func _bind_externals():
# %TS%_ink_player.bind_external_function("<function_name>", self, "_external_function")
#
#
# func _external_function(arg1, arg2):
# %TS%pass
# Uncomment to observe the variables from your ink story.
# You can observe multiple variables by putting adding them in the array.
# func _observe_variables():
# %TS%_ink_player.observe_variables(["var1", "var2"], self, "_variable_changed")
#
#
# func _variable_changed(variable_name, new_value):
# %TS%print("Variable '%s' changed to: %s" %[variable_name, new_value])

View file

@ -0,0 +1,107 @@
# warning-ignore-all:return_value_discarded
extends %BASE%
# ############################################################################ #
# Imports
# ############################################################################ #
var InkPlayer = load("res://addons/inkgd/ink_player.gd")
# ############################################################################ #
# Public Nodes
# ############################################################################ #
# Alternatively, it could also be retrieved from the tree.
# onready var _ink_player = $InkPlayer
@onready var _ink_player = InkPlayer.new()
# ############################################################################ #
# Lifecycle
# ############################################################################ #
func _ready():
%TS%# Adds the player to the tree.
%TS%add_child(_ink_player)
%TS%# Replace the example path with the path to your story.
%TS%# Remove this line if you set 'ink_file' in the inspector.
%TS%_ink_player.ink_file = load("res://path/to/file.ink.json")
%TS%# It's recommended to load the story in the background. On platforms that
%TS%# don't support threads, the value of this variable is ignored.
%TS%_ink_player.loads_in_background = true
%TS%_ink_player.connect("loaded", Callable(self, "_story_loaded"))
%TS%_ink_player.connect("continued", Callable(self, "_continued"))
%TS%_ink_player.connect("prompt_choices", Callable(self, "_prompt_choices"))
%TS%_ink_player.connect("ended", Callable(self, "_ended"))
%TS%# Creates the story. 'loaded' will be emitted once Ink is ready
%TS%# continue the story.
%TS%_ink_player.create_story()
# ############################################################################ #
# Signal Receivers
# ############################################################################ #
func _story_loaded(successfully: bool):
%TS%if !successfully:
%TS%%TS%return
%TS%# _observe_variables()
%TS%# _bind_externals()
%TS%# Here, the story is started immediately, but it could be started
%TS%# at a later time.
%TS%_ink_player.continue_story()
func _continued(text, tags):
%TS%print(text)
%TS%# Here you could yield for an hypothetical signal, before continuing.
%TS%# await self.event
%TS%_ink_player.continue_story()
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _prompt_choices(choices):
%TS%if !choices.is_empty():
%TS%%TS%print(choices)
%TS%%TS%# In a real world scenario, _select_choice' could be
%TS%%TS%# connected to a signal, like 'Button.pressed'.
%TS%%TS%_select_choice(0)
func _ended():
%TS%print("The End")
func _select_choice(index):
%TS%_ink_player.choose_choice_index(index)
%TS%_ink_player.continue_story()
# Uncomment to bind an external function.
#
# func _bind_externals():
# %TS%_ink_player.bind_external_function("<function_name>", self, "_external_function")
#
#
# func _external_function(arg1, arg2):
# %TS%pass
# Uncomment to observe the variables from your ink story.
# You can observe multiple variables by putting adding them in the array.
# func _observe_variables():
# %TS%_ink_player.observe_variables(["var1", "var2"], self, "_variable_changed")
#
#
# func _variable_changed(variable_name, new_value):
# %TS%print("Variable '%s' changed to: %s" %[variable_name, new_value])