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)