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

57
.gitattributes vendored Normal file
View file

@ -0,0 +1,57 @@
# Auto detect text files and perform LF normalization
* text=auto
# 3D models
*.3dm filter=lfs diff=lfs merge=lfs -text
*.3ds filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.c4d filter=lfs diff=lfs merge=lfs -text
*.collada filter=lfs diff=lfs merge=lfs -text
*.dae filter=lfs diff=lfs merge=lfs -text
*.dxf filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.jas filter=lfs diff=lfs merge=lfs -text
*.lws filter=lfs diff=lfs merge=lfs -text
*.lxo filter=lfs diff=lfs merge=lfs -text
*.ma filter=lfs diff=lfs merge=lfs -text
*.max filter=lfs diff=lfs merge=lfs -text
*.mb filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.ply filter=lfs diff=lfs merge=lfs -text
*.skp filter=lfs diff=lfs merge=lfs -text
*.stl filter=lfs diff=lfs merge=lfs -text
*.ztl filter=lfs diff=lfs merge=lfs -text
*.glb filter=lfs diff=lfs merge=lfs -text
# Audio
*.aif filter=lfs diff=lfs merge=lfs -text
*.aiff filter=lfs diff=lfs merge=lfs -text
*.it filter=lfs diff=lfs merge=lfs -text
*.mod filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.s3m filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.xm filter=lfs diff=lfs merge=lfs -text
*.bank filter=lfs diff=lfs merge=lfs -text
# Fonts
*.otf filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
# Images
*.bmp filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
*.iff filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
*.pict filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text
Assets/Fonts/Pangolin-Regular_SDF.asset filter=lfs diff=lfs merge=lfs -text
Assets/Fonts/LiberationSans_SDF.asset filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.so filter=lfs diff=lfs merge=lfs -text
*.dylib filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Godot 4+ specific ignores
.godot/

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# Figments of the Night
## Description
You wake up on a deserted island with no memory of who you are or what you are doing there. You will have to explore its landscapes, meet its other visitors and find clues in order to discover the truth behind this mysterious place…
or
you can simply relax in this paradise in the middle of the ocean and accept ignorance… whatever you do, do it before you have to go to sleep… to sleep…
## Honors
Indie Spain Jam 2023 — Top 25 Finalist
## The Team
> Gerard Gascón - Programming\
> Marc Batlle - Character Design, 3D Modeling & Narrative\
> Oriol Jaumot - Level Design & Environment Art\
> Pedro Astasio - SFXs & Music
## Game Jam
Indie Spain Jam 2023 — September 18th - 25th 2023 (1 week)
## Theme
The night falls
## Languages
Spanish

23
addons/inkgd/LICENSE Normal file
View file

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

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])

886
addons/inkgd/ink_player.gd Normal file
View file

@ -0,0 +1,886 @@
# ############################################################################ #
# 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 Node
class_name InkPlayer
# ############################################################################ #
# Imports
# ############################################################################ #
var InkRuntime = load("res://addons/inkgd/runtime.gd")
var InkResource = load("res://addons/inkgd/editor/import_plugins/ink_resource.gd")
var InkStory = load("res://addons/inkgd/runtime/story.gd")
var InkFunctionResult = load("res://addons/inkgd/runtime/extra/function_result.gd")
# ############################################################################ #
# Signals
# ############################################################################ #
## Emitted when the ink runtime encountered an exception. Exception are
## usually not recoverable as they corrupt the state. `stack_trace` is
## an optional PoolStringArray containing a stack trace, for logging purposes.
signal exception_raised(message, stack_trace)
## Emitted when the _story encountered an error. These errors are usually
## recoverable.
signal error_encountered(message, type)
## Emitted with `true` when the runtime had loaded the JSON file and created
## the _story. If an error was encountered, `successfully` will be `false` and
## and error will appear Godot's output.
signal loaded(successfully)
## Emitted with the text and tags of the current line when the _story
## successfully continued.
signal continued(text, tags)
## Emitted when using `continue_async`, if the time spent evaluating the ink
## exceeded the alloted time.
signal interrupted()
## Emitted when the player should pick a choice.
signal prompt_choices(choices)
## Emitted when a choice was reported back to the runtime.
signal choice_made(choice)
## Emitted when an external function is about to evaluate.
signal function_evaluating(function_name, arguments)
## Emitted when an external function evaluated.
signal function_evaluated(function_name, arguments, function_result)
## Emitted when a valid path string was choosen.
signal path_choosen(path, arguments)
## Emitted when the _story ended.
signal ended()
# ############################################################################ #
# Exported Properties
# ############################################################################ #
## The compiled Ink file (.json) to play.
@export var ink_file: Resource
## When `true` the _story will be created in a separate threads, to
## prevent the UI from freezing if the _story is too big. Note that
## on platforms where threads aren't available, the value of this
## property is ignored.
@export var loads_in_background: bool = true
# ############################################################################ #
# Properties
# ############################################################################ #
# These properties aren't exported because they depend on the runtime or the
# story to be set. The runtime insn't always available upon instantiation,
# and the story is only available after calling 'create_story' so rather than
# losing the values and confusing everybody, those properties are code-only.
## `true` to allow external function fallbacks, `false` otherwise. If this
## property is `false` and the appropriate function hasn't been binded, the
## _story will output an error.
var allow_external_function_fallbacks: bool: get = get_aeff, set = set_aeff
func set_aeff(value: bool):
if _story == null:
_push_null_story_error()
return
_story.allow_external_function_fallbacks = value
func get_aeff() -> bool:
if _story == null:
_push_null_story_error()
return false
return _story.allow_external_function_fallbacks
# skips saving global values that remain equal to the initial values that were
# declared in Ink.
var do_not_save_default_values: bool: get = get_dnsdv, set = set_dnsdv
func set_dnsdv(value: bool):
var ink_runtime = _ink_runtime.get_ref()
if ink_runtime == null:
_push_null_runtime_error()
return false
ink_runtime.dont_save_default_values = value
func get_dnsdv() -> bool:
var ink_runtime = _ink_runtime.get_ref()
if ink_runtime == null:
_push_null_runtime_error()
return false
return ink_runtime.dont_save_default_values
## Uses `assert` instead of `push_error` to report critical errors, thus
## making them more explicit during development.
var stop_execution_on_exception: bool: get = get_seoex, set = set_seoex
func set_seoex(value: bool):
var ink_runtime = _ink_runtime.get_ref()
if ink_runtime == null:
_push_null_runtime_error()
return
ink_runtime.stop_execution_on_exception = value
func get_seoex() -> bool:
var ink_runtime = _ink_runtime.get_ref()
if ink_runtime == null:
_push_null_runtime_error()
return false
return ink_runtime.stop_execution_on_exception
## Uses `assert` instead of `push_error` to report _story errors, thus
## making them more explicit during development.
var stop_execution_on_error: bool: get = get_seoer, set = set_seoer
func set_seoer(value: bool):
var ink_runtime = _ink_runtime.get_ref()
if ink_runtime == null:
_push_null_runtime_error()
return
ink_runtime.stop_execution_on_error = value
func get_seoer() -> bool:
var ink_runtime = _ink_runtime.get_ref()
if ink_runtime == null:
_push_null_runtime_error()
return false
return ink_runtime.stop_execution_on_error
# ############################################################################ #
# Read-only Properties
# ############################################################################ #
## `true` if the _story can continue (i. e. is not expecting a choice to be
## choosen and hasn't reached the end).
var can_continue: bool: get = get_can_continue
func get_can_continue() -> bool:
if _story == null:
_push_null_story_error()
return false
return _story.can_continue
## If `continue_async` was called (with milliseconds limit > 0) then this
## property will return false if the ink evaluation isn't yet finished, and
## you need to call it again in order for the continue to fully complete.
var async_continue_complete: bool: get = get_async_continue_complete
func get_async_continue_complete() -> bool:
if _story == null:
_push_null_story_error()
return false
return _story.async_continue_complete
## The content of the current line.
var current_text: String: get = get_current_text
func get_current_text() -> String:
if _story == null:
_push_null_story_error()
return ""
return _story.current_text
## The current choices. Empty is there are no choices for the current line.
var current_choices: Array: get = get_current_choices
func get_current_choices() -> Array:
if _story == null:
_push_null_story_error()
return []
return _story.current_choices.duplicate()
## The current tags. Empty is there are no tags for the current line.
var current_tags: Array: get = get_current_tags
func get_current_tags() -> Array:
if _story == null:
_push_null_story_error()
return []
if _story.current_tags == null:
return []
return _story.current_tags
## The global tags for the _story. Empty if none have been declared.
var global_tags: Array: get = get_global_tags
func get_global_tags() -> Array:
if _story == null:
_push_null_story_error()
return []
if _story.global_tags == null:
return []
return _story.global_tags
## `true` if the _story currently has choices, `false` otherwise.
var has_choices: bool: get = get_has_choices
func get_has_choices() -> bool:
return !self.current_choices.is_empty()
## The name of the current flow.
var current_flow_name: String: get = get_current_flow_name
func get_current_flow_name() -> String:
if _story == null:
_push_null_story_error()
return ""
return _story.state.current_flow_name
## The names of all flows currently alive.
var alive_flow_names: Array: get = get_alive_flow_names
func get_alive_flow_names() -> Array:
if _story == null:
_push_null_story_error()
return []
return _story.alive_flow_names
## `true` if the current flow is the default flow.
var current_flow_is_default_flow: bool: get = get_current_flow_is_default_flow
func get_current_flow_is_default_flow() -> bool:
if _story == null:
_push_null_story_error()
return false
return _story.current_flow_is_default_flow
## The current story path.
var current_path: String: get = get_current_path
func get_current_path() -> String:
if _story == null:
_push_null_story_error()
return ""
var path = _story.state.current_path_string
if path == null:
return ""
else:
return path
# ############################################################################ #
# Private Properties
# ############################################################################ #
var _ink_runtime: WeakRef = WeakRef.new()
var _story: InkStory = null
var _thread: Thread
var _manages_runtime: bool = false
# ############################################################################ #
# Initialization
# ############################################################################ #
func _init():
name = "InkPlayer"
# ############################################################################ #
# Overrides
# ############################################################################ #
func _exit_tree():
call_deferred("_remove_runtime")
# ############################################################################ #
# Methods
# ############################################################################ #
## Creates the _story, based on the value of `ink_file`. The result of this
## method is reported through the 'story_loaded' signal.
func create_story() -> int:
if ink_file == null:
_push_error("'ink_file' is null, did Godot import the resource correctly?")
call_deferred("emit_signal", "loaded", false)
return ERR_CANT_CREATE
if !("json" in ink_file) || typeof(ink_file.json) != TYPE_STRING:
_push_error(
"'ink_file' doesn't have the appropriate resource type." + \
"Are you sure you imported a JSON file?"
)
call_deferred("emit_signal", "loaded", false)
return ERR_CANT_CREATE
if loads_in_background && _current_platform_supports_threads():
_thread = Thread.new()
var error = _thread.start(_async_create_story.bind(ink_file.json, _ink_runtime.get_ref()))
if error != OK:
printerr("[inkgd] [ERROR] Could not start the thread: error code %d", error)
call_deferred("emit_signal", "loaded", false)
return error
else:
return OK
else:
call_deferred("_create_and_finalize_story", ink_file.json, _ink_runtime.get_ref())
return OK
## Reset the Story back to its initial state as it was when it was
## first constructed.
func reset() -> void:
if _story == null:
_push_null_story_error()
return
_story.reset_state()
## Destroys the current story.
func destroy() -> void:
_story = null
# ############################################################################ #
# Methods | Story Flow
# ############################################################################ #
## Continues the story.
func continue_story() -> String:
if _story == null:
_push_null_story_error()
return ""
var text: String = ""
if self.can_continue:
_story.continue_story()
text = self.current_text
elif self.has_choices:
emit_signal("prompt_choices", self.current_choices)
else:
emit_signal("ended")
return text
## An "asynchronous" version of `continue_story` that only partially evaluates
## the ink, with a budget of a certain time limit. It will exit ink evaluation
## early if the evaluation isn't complete within the time limit, with the
## `async_continue_complete` property being false. This is useful if the
## evaluation takes a long time, and you want to distribute it over multiple
## game frames for smoother animation. If you pass a limit of zero, then it will
## fully evaluate the ink in the same way as calling continue_story.
##
## To get notified when the evaluation is exited early, you can connect to the
## `interrupted` signal.
func continue_story_async(millisecs_limit_async: float) -> void:
if _story == null:
_push_null_story_error()
return
if self.can_continue:
_story.continue_async(millisecs_limit_async)
if !self.async_continue_complete:
emit_signal("interrupted")
return
elif self.has_choices:
emit_signal("prompt_choices", self.current_choices)
else:
emit_signal("ended")
## Continue the story until the next choice point or until it runs out of
## content. This is as opposed to `continue` which only evaluates one line
## of output at a time. It returns the resulting text evaluated by the ink
## engine, concatenated together.
func continue_story_maximally() -> String:
if _story == null:
_push_null_story_error()
return ""
var text: String = ""
if self.can_continue:
_story.continue_story_maximally()
text = self.current_text
elif self.has_choices:
emit_signal("prompt_choices", self.current_choices)
else:
emit_signal("ended")
return text
## Chooses a choice. If the _story is not currently expected choices or
## the index is out of bounds, this method does nothing.
func choose_choice_index(index: int) -> void:
if _story == null:
_push_null_story_error()
return
if index >= 0 && index < self.current_choices.size():
_story.choose_choice_index(index);
## Moves the _story to the specified knot/stitch/gather. This method
## will throw an error through the 'exception' signal if the path string
## does not match any known path.
func choose_path(path: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.choose_path_string(path)
## Switches the flow, creating a new flow if it doesn't exist.
func switch_flow(flow_name: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.switch_flow(flow_name)
## Switches the the default flow.
func switch_to_default_flow() -> void:
if _story == null:
_push_null_story_error()
return
_story.switch_to_default_flow()
## Remove the given flow.
func remove_flow(flow_name: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.remove_flow(flow_name)
# ############################################################################ #
# Methods | Tags
# ############################################################################ #
## Returns the tags declared at the given path.
func tags_for_content_at_path(path: String) -> Array:
if _story == null:
_push_null_story_error()
return []
return _story.tags_for_content_at_path(path)
# ############################################################################ #
# Methods | Visit Count
# ############################################################################ #
## Returns the visit count of the given path.
func visit_count_at_path(path: String) -> int:
if _story == null:
_push_null_story_error()
return 0
return _story.state.visit_count_at_path_string(path)
# ############################################################################ #
# Methods | State Management
# ############################################################################ #
## Gets the current state as a JSON string. It can then be saved somewhere.
func get_state() -> String:
if _story == null:
_push_null_story_error()
return ""
return _story.state.to_json()
## If you have a large story, and saving state to JSON takes too long for your
## framerate, you can temporarily freeze a copy of the state for saving on
## a separate thread. Internally, the engine maintains a "diff patch".
## When you've finished saving your state, call `background_save_complete`
## and that diff patch will be applied, allowing the story to continue
## in its usual mode.
func copy_state_for_background_thread_save() -> String:
if _story == null:
_push_null_story_error()
return ""
return _story.copy_state_for_background_thread_save().to_json()
## See `copy_state_for_background_thread_save`. This method releases the
## "frozen" save state, applying its patch that it was using internally.
func background_save_complete() -> void:
if _story == null:
_push_null_story_error()
return
_story.background_save_complete()
## Sets the state from a JSON string.
func set_state(state: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.state.load_json(state)
## Saves the current state to the given path.
func save_state_to_path(path: String):
if _story == null:
_push_null_story_error()
return
if !path.begins_with("res://") && !path.begins_with("user://"):
path = "user://%s" % path
var file := FileAccess.open(path, FileAccess.WRITE)
save_state_to_file(file)
file.close()
## Saves the current state to the file.
func save_state_to_file(file: FileAccess):
if _story == null:
_push_null_story_error()
return
if file.is_open():
file.store_string(get_state())
# TODO: Add save and load in background
## Loads the state from the given path.
func load_state_from_path(path: String):
if _story == null:
_push_null_story_error()
return
if !path.begins_with("res://") && !path.begins_with("user://"):
path = "user://%s" % path
var file := FileAccess.open(path, FileAccess.READ)
load_state_from_file(file)
file.close()
## Loads the state from the given file.
func load_state_from_file(file: FileAccess):
if _story == null:
_push_null_story_error()
return
if !file.is_open():
return
file.seek(0);
if file.get_length() > 0:
_story.state.load_json(file.get_as_text())
# ############################################################################ #
# Methods | Variables
# ############################################################################ #
## Returns the value of variable named 'name' or 'null' if it doesn't exist.
@warning_ignore("shadowed_variable_base_class")
func get_variable(name: String):
if _story == null:
_push_null_story_error()
return null
return _story.variables_state.get_variable(name)
## Sets the value of variable named 'name'.
@warning_ignore("shadowed_variable_base_class")
func set_variable(name: String, value):
if _story == null:
_push_null_story_error()
return
_story.variables_state.set_variable(name, value)
# ############################################################################ #
# Methods | Variable Observers
# ############################################################################ #
## Registers an observer for the given variables.
func observe_variables(variable_names: Array, object: Object, method_name: String):
if _story == null:
_push_null_story_error()
return
_story.observe_variables(variable_names, object, method_name)
## Registers an observer for the given variable.
func observe_variable(variable_name: String, object: Object, method_name: String):
if _story == null:
_push_null_story_error()
return
_story.observe_variable(variable_name, object, method_name)
## Removes an observer for the given variable name. This method is highly
## specific and will only remove one observer.
func remove_variable_observer(object: Object, method_name: String, specific_variable_name: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.remove_variable_observer(object, method_name, specific_variable_name)
## Removes all observers registered with the couple object/method_name,
## regardless of which variable they observed.
func remove_variable_observer_for_all_variables(object: Object, method_name: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.remove_variable_observer(object, method_name)
## Removes all observers observing the given variable.
func remove_all_variable_observers(specific_variable_name: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.remove_variable_observer(null, null, specific_variable_name)
# ############################################################################ #
# Methods | External Functions
# ############################################################################ #
## Binds an external function.
func bind_external_function(
func_name: String,
object: Object,
method_name: String,
lookahead_safe = false
) -> void:
if _story == null:
_push_null_story_error()
return
_story.bind_external_function(func_name, object, method_name, lookahead_safe)
## Unbinds an external function.
func unbind_external_function(func_name: String) -> void:
if _story == null:
_push_null_story_error()
return
_story.unbind_external_function(func_name)
# ############################################################################ #
# Methods | Functions
# ############################################################################ #
func has_function(function_name: String) -> bool:
return _story.has_function(function_name)
## Evaluate a given ink function, returning its return value (but not
## its output).
func evaluate_function(function_name: String, arguments = []) -> InkFunctionResult:
if _story == null:
_push_null_story_error()
return null
var result = _story.evaluate_function(function_name, arguments, true)
if result != null:
return InkFunctionResult.new(result["output"], result["result"])
else:
return null
# ############################################################################ #
# Methods | Ink List Creation
# ############################################################################ #
## Creates a new empty InkList that's intended to hold items from a particular
## origin list definition.
func create_ink_list_with_origin(single_origin_list_name: String) -> InkList:
return InkList.new_with_origin(single_origin_list_name, _story)
## Creates a new InkList from the name of a preexisting item.
func create_ink_list_from_item_name(item_name: String) -> InkList:
return InkList.from_string(item_name, _story)
# ############################################################################ #
# Private Methods | Signal Forwarding
# ############################################################################ #
func _exception_raised(message, stack_trace) -> void:
emit_signal("exception_raised", message, stack_trace)
func _on_error(message, type) -> void:
if get_signal_connection_list("error_encountered").size() == 0:
_push_story_error(message, type)
else:
emit_signal("error_encountered", message, type)
func _on_did_continue() -> void:
emit_signal("continued", self.current_text, self.current_tags)
func _on_make_choice(choice) -> void:
emit_signal("choice_made", choice.text)
func _on_evaluate_function(function_name, arguments) -> void:
emit_signal("function_evaluating", function_name, arguments)
func _on_complete_evaluate_function(function_name, arguments, text_output, return_value) -> void:
var function_result = InkFunctionResult.new(text_output, return_value)
emit_signal("function_evaluated", function_name, arguments, function_result)
func _on_choose_path_string(path, arguments) -> void:
emit_signal("path_choosen", path, arguments)
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _create_story(json_story, runtime) -> void:
_story = InkStory.new(json_story, runtime)
func _async_create_story(json_story, runtime) -> void:
_create_story(json_story, runtime)
call_deferred("_async_creation_completed")
func _async_creation_completed() -> void:
_thread.wait_to_finish()
_thread = null
_finalise_story_creation()
func _create_and_finalize_story(json_story, runtime) -> void:
_create_story(json_story, runtime)
_finalise_story_creation()
func _finalise_story_creation() -> void:
_story.connect("on_error", Callable(self, "_on_error"))
_story.connect("on_did_continue", Callable(self, "_on_did_continue"))
_story.connect("on_make_choice", Callable(self, "_on_make_choice"))
_story.connect("on_evaluate_function", Callable(self, "_on_evaluate_function"))
_story.connect("on_complete_evaluate_function", Callable(self, "_on_complete_evaluate_function"))
_story.connect("on_choose_path_string", Callable(self, "_on_choose_path_string"))
var ink_runtime = _ink_runtime.get_ref()
if ink_runtime == null:
_push_null_runtime_error()
emit_signal("loaded", false)
return
emit_signal("loaded", true)
func _add_runtime(root) -> void:
# The InkRuntime is normaly an auto-loaded singleton,
# but if it's not present, it's added here.
var runtime: Node
if root.has_node("__InkRuntime"):
runtime = root.get_node("__InkRuntime")
else:
_manages_runtime = true
runtime = InkRuntime.init(root)
if !runtime.is_connected("exception_raised", _exception_raised):
runtime.connect("exception_raised", _exception_raised)
_ink_runtime = weakref(runtime)
func _remove_runtime() -> void:
if _manages_runtime:
InkRuntime.deinit(get_tree().root)
func _current_platform_supports_threads() -> bool:
return OS.get_name() != "HTML5"
func _push_null_runtime_error() -> void:
_push_error(
"InkRuntime could not found, did you remove it from the tree?"
)
func _push_null_story_error() -> void:
_push_error("The _story is 'null', was it loaded properly?")
func _push_story_error(message: String, type: int) -> void:
if Engine.is_editor_hint():
match type:
Ink.ErrorType.ERROR:
printerr(message)
Ink.ErrorType.WARNING, Ink.ErrorType.AUTHOR:
print(message)
else:
match type:
Ink.ErrorType.ERROR:
push_error(message)
Ink.ErrorType.WARNING, Ink.ErrorType.AUTHOR:
push_warning(message)
func _push_error(message: String):
if Engine.is_editor_hint():
printerr(message)
var i = 1
for stack_element in get_stack():
if i <= 2:
i += 1
continue
printerr(
" ", (i - 2), " - ", stack_element["source"], ":",
stack_element["line"], " - at function: ", stack_element["function"]
)
else:
push_error(message)

View file

@ -0,0 +1,49 @@
@tool
# ############################################################################ #
# Copyright © 2018-2021 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 RefCounted
class_name InkPlayerFactory
const DO_NOT_USE_MONO_RUNTIME_SETTING = "inkgd/do_not_use_mono_runtime"
# ############################################################################ #
# Methods
# ############################################################################ #
static func create():
if _should_use_mono():
var InkPlayer = load("res://addons/inkgd/mono/InkPlayer.cs")
if InkPlayer.can_instantiate():
return InkPlayer.new()
else:
printerr(
"[inkgd] [ERROR] InkPlayer can't be instantiated. Make sure that a suitable " +
"copy of 'ink-runtime-engine.dll' can be found in project and double check " +
"that the .csproj file contains a <RefCounted> item pointing to it. " +
"If everything is configured correctly, you may need to rebuild " +
"the C# solution. Please refer to [TO BE ADDED] for additional help."
)
print("[inkgd] [INFO] Falling back to the GDScript runtime.")
# Falling back to GDscript.
return load("res://addons/inkgd/ink_player.gd").new()
static func _should_use_mono() -> bool:
if ProjectSettings.has_setting(DO_NOT_USE_MONO_RUNTIME_SETTING):
var do_not_use_mono = ProjectSettings.get_setting(DO_NOT_USE_MONO_RUNTIME_SETTING)
if do_not_use_mono == null:
do_not_use_mono = false
return _can_run_mono() && !do_not_use_mono
else:
return _can_run_mono()
static func _can_run_mono() -> bool:
return type_exists("_GodotSharp")

View file

@ -0,0 +1,182 @@
// /////////////////////////////////////////////////////////////////////////// /
// Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
// Licensed under the MIT License.
// See LICENSE in the project root for license information.
// /////////////////////////////////////////////////////////////////////////// /
using Godot;
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
public partial class InkBridger : Node
{
#region Imports
private readonly GDScript InkPath =
(GDScript) ResourceLoader.Load("res://addons/inkgd/runtime/ink_path.gd");
private readonly GDScript InkList =
(GDScript) ResourceLoader.Load("res://addons/inkgd/runtime/lists/ink_list.gd");
private readonly GDScript InkListDefinition =
(GDScript) ResourceLoader.Load("res://addons/inkgd/runtime/lists/list_definition.gd");
private readonly GDScript InkListItem =
(GDScript) ResourceLoader.Load("res://addons/inkgd/runtime/lists/structs/ink_list_item.gd");
private readonly GDScript InkFunctionResult =
(GDScript) ResourceLoader.Load("res://addons/inkgd/runtime/extra/function_result.gd");
#endregion
#region Methods | Helpers
public bool IsInkObjectOfType(Godot.Object inkObject, string name)
{
return inkObject.HasMethod("is_ink_class") && (bool)inkObject.Call("is_ink_class", new object[] { name });
}
public Godot.Object MakeFunctionResult(string textOutput, object returnValue)
{
var parameters = new object[] { textOutput ?? "", returnValue };
return (Godot.Object) InkFunctionResult.New(parameters);
}
#endregion
#region Methods | Conversion -> (GDScript -> C#)
public Godot.Object MakeGDInkPath(Ink.Runtime.Path3D path) {
var inkPath = (Godot.Object) InkPath.New();
inkPath.Call("_init_with_components_string", path.componentsString);
return inkPath;
}
public Godot.Object MakeGDInkList(Ink.Runtime.InkList list)
{
var inkListBase = new Godot.Collections.Dictionary<string, int>();
foreach(KeyValuePair<Ink.Runtime.InkListItem, int> kv in list) {
inkListBase.Add(MakeGDInkListItem(kv.Key).Call("serialized") as string, kv.Value);
}
object[] inkListParams = new object[] {
inkListBase,
list.originNames.ToArray(),
MakeGDInkListOrigins(list.origins)
};
var inkList = (Godot.Object) InkList.New();
inkList.Call("_init_from_csharp", inkListParams);
return inkList;
}
public Ink.Runtime.Path3D MakeSharpInkPath(Godot.Object path) {
if (!IsInkObjectOfType(path, "InkPath"))
{
throw new ArgumentException("Expected a 'Godot.Object' of class 'InkPath'");
}
return new Ink.Runtime.Path3D((string)path.Get("components_string"));
}
#endregion
#region Methods | Conversion (GDScript -> C#)
public Ink.Runtime.InkList MakeSharpInkList(Godot.Object list, Ink.Runtime.Story story)
{
if (!IsInkObjectOfType(list, "InkList"))
{
throw new ArgumentException("Expected a 'Godot.Object' of class 'InkList'");
}
var underlyingDictionary = new Godot.Collections.Dictionary<string, int>(
(Godot.Collections.Dictionary)list.Get("_dictionary"));
var originNames = new Godot.Collections.Array<string>(
(Godot.Collections.Array)list.Get("origin_names"));
var inkList = new Ink.Runtime.InkList();
inkList.origins = new List<Ink.Runtime.ListDefinition>();
inkList.SetInitialOriginNames(originNames.ToList());
foreach(string originName in originNames)
{
if (story.listDefinitions.TryListGetDefinition (originName, out Ink.Runtime.ListDefinition definition))
{
if (!inkList.origins.Contains(definition)) {
inkList.origins.Add(definition);
}
}
else
{
throw new Exception (
$"InkList origin could not be found in story when reconstructing list: {originName}"
);
}
}
foreach(KeyValuePair<string, int> kv in underlyingDictionary)
{
inkList[MakeSharpInkListItem(kv.Key)] = kv.Value;
}
return inkList;
}
#endregion
#region Private Methods | Conversion (C# -> GDScript)
private Godot.Collections.Array<Godot.Object> MakeGDInkListOrigins(
List<Ink.Runtime.ListDefinition> listDefinitions)
{
var inkListDefinitions = new Godot.Collections.Array<Godot.Object>();
foreach(Ink.Runtime.ListDefinition listDefinition in listDefinitions) {
var inkListDefinition = MakeGDListDefinition(listDefinition);
inkListDefinitions.Add(inkListDefinition);
}
return inkListDefinitions;
}
private Godot.Object MakeGDListDefinition(Ink.Runtime.ListDefinition listDefinition)
{
var items = new Godot.Collections.Dictionary<Godot.Object, int>();
foreach(KeyValuePair<Ink.Runtime.InkListItem, int> kv in listDefinition.items) {
var inkListItem = MakeGDInkListItem(kv.Key);
items.Add(inkListItem, kv.Value);
}
var definitionParams = new object[] { listDefinition.name, items };
var inkListDefinition = (Godot.Object) InkListDefinition.New(definitionParams);
return inkListDefinition;
}
private Godot.Object MakeGDInkListItem(Ink.Runtime.InkListItem listItem)
{
object[] itemParams = new object[] { listItem.fullName };
var inkListItem = (Godot.Object) InkListItem.New();
inkListItem.Call("_init_with_full_name", itemParams);
return inkListItem;
}
#endregion
#region Private Methods | Conversion (GDScript -> C#)
private Ink.Runtime.InkListItem MakeSharpInkListItem(string listItemKey)
{
var listItem = (Godot.Object) InkListItem.Call("from_serialized_key", new object[] { listItemKey });
if (!IsInkObjectOfType(listItem, "InkListItem")) {
throw new ArgumentException("Expected a 'Godot.Object' of class 'InkListItem'");
}
return new Ink.Runtime.InkListItem(
listItem.Get("origin_name") as string,
listItem.Get("item_name") as string
);
}
#endregion
}

File diff suppressed because it is too large Load diff

View file

7
addons/inkgd/plugin.cfg Normal file
View file

@ -0,0 +1,7 @@
[plugin]
name="InkGD"
description="Full implementation of inkle's Ink narrative language in pure GDScript, with editor support."
author="Frédéric Maquin"
version="0.6.0"
script="editor/ink_editor_plugin.gd"

35
addons/inkgd/runtime.gd Normal file
View file

@ -0,0 +1,35 @@
# ############################################################################ #
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends Node
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkRuntime
static func init(root_node, stop_on_error = true):
if root_node.has_node("__InkRuntime"):
var _ink_runtime = root_node.get_node("__InkRuntime")
_ink_runtime.stop_execution_on_exception = stop_on_error
_ink_runtime.stop_execution_on_error = stop_on_error
return _ink_runtime
var _ink_runtime = load("res://addons/inkgd/runtime/static/ink_runtime.gd").new()
_ink_runtime.stop_execution_on_exception = stop_on_error
_ink_runtime.stop_execution_on_error = stop_on_error
root_node.add_child(_ink_runtime)
return _ink_runtime
static func deinit(root_node):
var _ink_runtime = root_node.get_node("__InkRuntime")
root_node.remove_child(_ink_runtime)
_ink_runtime.queue_free()

View file

@ -0,0 +1,457 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkCallStack
# ############################################################################ #
class Element extends InkBase:
var current_pointer: InkPointer = InkPointer.null_pointer
var in_expression_evaluation: bool = false
var temporary_variables = null # Dictionary<String, InkObject>
var type: int = 0 # Ink.PushPopType
var evaluation_stack_height_when_pushed: int = 0
var function_start_in_ouput_stream: int = 0
# (Ink.PushPopType, Pointer, bool) -> InkElement
func _init(type, pointer, in_expression_evaluation = false):
self.current_pointer = pointer
self.in_expression_evaluation = in_expression_evaluation
self.temporary_variables = {}
self.type = type
# () -> InkElement
func copy():
var copy = Element.new(self.type, self.current_pointer, self.in_expression_evaluation)
copy.temporary_variables = self.temporary_variables.duplicate()
copy.evaluation_stack_height_when_pushed = evaluation_stack_height_when_pushed
copy.function_start_in_ouput_stream = function_start_in_ouput_stream
return copy
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "CallStack.Element" || super.is_ink_class(type)
func get_ink_class():
return "CallStack.Element"
class InkThread extends InkBase:
var callstack = null # Array<Element>
var thread_index: int = 0 # int
var previous_pointer: InkPointer = InkPointer.null_pointer
func _init(static_json = null):
get_static_json(static_json)
callstack = []
# Dictionary<string, object>, Story
func _init_with(jthread_obj, story_context):
thread_index = int(jthread_obj["threadIndex"])
var jthread_callstack = jthread_obj["callstack"]
for jel_tok in jthread_callstack:
var jelement_obj = jel_tok
var push_pop_type = int(jelement_obj["type"])
var pointer = InkPointer.null_pointer
var current_container_path_str = null
var current_container_path_str_token = null
if jelement_obj.has("cPath"):
current_container_path_str_token = jelement_obj["cPath"]
current_container_path_str = str(current_container_path_str_token)
var thread_pointer_result = story_context.content_at_path(InkPath.new_with_components_string(current_container_path_str))
pointer = InkPointer.new(thread_pointer_result.container, int(jelement_obj["idx"]))
if thread_pointer_result.obj == null:
InkUtils.throw_exception(
"When loading state, internal story location " +
"couldn't be found: '%s'. " % current_container_path_str +
"Has the story changed since this save data was created?"
)
return
elif thread_pointer_result.approximate:
story_context.warning(
"When loading state, exact internal story location " +
"couldn't be found: '%s', so it was" % current_container_path_str +
"approximated to '%s' " + pointer.container.path._to_string() +
"to recover. Has the story changed since this save data was created?"
)
var in_expression_evaluation = bool(jelement_obj["exp"])
var el = Element.new(push_pop_type, pointer, in_expression_evaluation)
var temps
if jelement_obj.has("temp"):
temps = jelement_obj["temp"] # Dictionary<string, object>
el.temporary_variables = self.StaticJSON.jobject_to_dictionary_runtime_objs(temps)
else:
el.temporary_variables.clear()
callstack.append(el)
var prev_content_obj_path
if jthread_obj.has("previousContentObject"):
prev_content_obj_path = str(jthread_obj["previousContentObject"])
var prev_path = InkPath.new_with_components_string(prev_content_obj_path)
self.previous_pointer = story_context.pointer_at_path(prev_path)
# () -> InkThread
func copy():
var copy = InkThread.new(self.StaticJSON)
copy.thread_index = self.thread_index
for e in callstack:
copy.callstack.append(e.copy())
copy.previous_pointer = self.previous_pointer
return copy
# (SimpleJson.Writer) -> void
func write_json(writer):
writer.write_object_start()
writer.write_property_start("callstack")
writer.write_array_start()
for el in self.callstack:
writer.write_object_start()
if !el.current_pointer.is_null:
writer.write_property("cPath", el.current_pointer.container.path.components_string)
writer.write_property("idx", el.current_pointer.index)
writer.write_property("exp", el.in_expression_evaluation)
writer.write_property("type", int(el.type))
if el.temporary_variables.size() > 0:
writer.write_property_start("temp")
self.StaticJSON.write_dictionary_runtime_objs(writer, el.temporary_variables)
writer.write_property_end()
writer.write_object_end()
writer.write_array_end()
writer.write_property_end()
writer.write_property("threadIndex", self.thread_index)
if !self.previous_pointer.is_null:
writer.write_property("previousContentObject", self.previous_pointer.resolve().path._to_string())
writer.write_object_end()
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "CallStack.InkThread" || super.is_ink_class(type)
func get_ink_class():
return "CallStack.InkThread"
# ######################################################################## #
static func new_with(jthread_obj, story_context, static_json = null):
var thread = InkThread.new(static_json)
thread._init_with(jthread_obj, story_context)
return thread
# ######################################################################## #
var StaticJSON: InkStaticJSON:
get: return _static_json.get_ref()
var _static_json = WeakRef.new()
func get_static_json(static_json = null):
if static_json != null:
_static_json = weakref(static_json)
return
var InkRuntime = Engine.get_main_loop().root.get_node("__InkRuntime")
InkUtils.__assert__(InkRuntime != null,
str("[InkCallStack.InkThread] Could not retrieve 'InkRuntime' singleton from the scene tree."))
_static_json = weakref(InkRuntime.json)
# () -> Array<InkElement>
var elements : get = get_elements
func get_elements():
return self.callstack
# () -> int
var depth : get = get_depth
func get_depth():
return self.elements.size()
# () -> InkElement
var current_element : get = get_current_element
func get_current_element():
var thread = self._threads.back()
var cs = thread.callstack
return cs.back()
# () -> int
var current_element_index : get = get_current_element_index
func get_current_element_index():
return self.callstack.size() - 1
# () -> InkThread
# (InkThread) -> void
var current_thread : get = get_current_thread, set = set_current_thread
func get_current_thread():
return self._threads.back()
func set_current_thread(value):
InkUtils.__assert__(_threads.size() == 1,
"Shouldn't be directly setting the current thread when we have a stack of them")
self._threads.clear()
self._threads.append(value)
# () -> bool
var can_pop : get = get_can_pop
func get_can_pop():
return self.callstack.size() > 1
# (InkStory | CallStack) -> CallStack
func _init(story_context_or_to_copy, static_json = null):
get_static_json(static_json)
if story_context_or_to_copy.is_ink_class("Story"):
var story_context = story_context_or_to_copy
_start_of_root = InkPointer.start_of(story_context.root_content_container)
reset()
elif story_context_or_to_copy.is_ink_class("CallStack"):
var to_copy = story_context_or_to_copy
self._threads = []
for other_thread in to_copy._threads:
self._threads.append(other_thread.copy())
self._thread_counter = to_copy._thread_counter
self._start_of_root = to_copy._start_of_root
# () -> void
func reset():
self._threads = []
self._threads.append(InkThread.new(self.StaticJSON))
self._threads[0].callstack.append(Element.new(Ink.PushPopType.TUNNEL, self._start_of_root))
# (Dictionary<string, object>, InkStory) -> void
func set_json_token(jobject, story_context):
self._threads.clear()
var jthreads = jobject["threads"]
for jthread_tok in jthreads:
var jthread_obj = jthread_tok
var thread = InkThread.new_with(jthread_obj, story_context)
self._threads.append(thread)
self._thread_counter = int(jobject["threadCounter"])
self._start_of_root = InkPointer.start_of(story_context.root_content_container)
# (SimpleJson.Writer) -> void
func write_json(writer):
writer.write_object(Callable(self, "_anonymous_write_json"))
# () -> void
func push_thread():
var new_thread = self.current_thread.copy()
self._thread_counter += 1
new_thread.thread_index = self._thread_counter
self._threads.append(new_thread)
# () -> void
func fork_thread():
var forked_thread = self.current_thread.copy()
self._thread_counter += 1
forked_thread.thread_index = self._thread_counter
return forked_thread
# () -> void
func pop_thread():
if self.can_pop_thread:
self._threads.erase(self.current_thread)
else:
InkUtils.throw_exception("Can't pop thread")
# () -> bool
var can_pop_thread : get = get_can_pop_thread
func get_can_pop_thread():
return _threads.size() > 1 && !self.element_is_evaluate_from_game
# () -> bool
var element_is_evaluate_from_game : get = get_element_is_evaluate_from_game
func get_element_is_evaluate_from_game():
return self.current_element.type == Ink.PushPopType.FUNCTION_EVALUATION_FROM_GAME
# (Ink.PushPopType, int, int) -> void
func push(type, external_evaluation_stack_height = 0, output_stream_length_with_pushed = 0):
var element = Element.new(type, self.current_element.current_pointer, false)
element.evaluation_stack_height_when_pushed = external_evaluation_stack_height
element.function_start_in_ouput_stream = output_stream_length_with_pushed
self.callstack.append(element)
# (Ink.PushPopType | null) -> void
func can_pop_type(type = null):
if !self.can_pop:
return false
if type == null:
return true
return self.current_element.type == type
# (Ink.PushPopType | null) -> void
func pop(type = null):
if can_pop_type(type):
self.callstack.pop_back()
return
else:
InkUtils.throw_exception("Mismatched push/pop in Callstack")
# (String, int) -> InkObject
func get_temporary_variable_with_name(name, context_index = -1) -> InkObject:
if context_index == -1:
context_index = self.current_element_index + 1
var var_value = null
var context_element = self.callstack[context_index - 1]
if context_element.temporary_variables.has(name):
var_value = context_element.temporary_variables[name]
return var_value
else:
return null
# (String, InkObject, bool, int) -> void
func set_temporary_variable(name, value, declare_new, context_index = -1):
if context_index == -1:
context_index = self.current_element_index + 1
var context_element = self.callstack[context_index - 1]
if !declare_new && !context_element.temporary_variables.has(name):
InkUtils.throw_exception("Could not find temporary variable to set: %s" % name)
return
if context_element.temporary_variables.has(name):
var old_value = context_element.temporary_variables[name]
InkListValue.retain_list_origins_for_assignment(old_value, value)
context_element.temporary_variables[name] = value
# (String) -> int
func context_for_variable_named(name):
if self.current_element.temporary_variables.has(name):
return self.current_element_index + 1
else:
return 0
# (int) -> InkThread | null
func thread_with_index(index):
for thread in self._threads:
if thread.thread_index == index:
return thread
return null
var callstack : get = get_callstack
func get_callstack():
return self.current_thread.callstack
var callstack_trace : get = get_callstack_trace
func get_callstack_trace():
var sb = ""
var t = 0
while t < _threads.size():
var thread = _threads[t]
var is_current = (t == _threads.size() - 1)
sb += str("=== THREAD ", str(t + 1), "/", str(_threads.size()), " ",
("(current) " if is_current else "" ), "===\n")
var i = 0
while i < thread.callstack.size():
if thread.callstack[i].type == Ink.PushPopType.FUNCTION:
sb += " [FUNCTION] "
else:
sb += " [TUNNEL] "
var pointer = thread.callstack[i].current_pointer
if !pointer.is_null:
sb += "<SOMEWHERE IN "
sb += pointer.container.path._to_string()
sb += "\n>"
i += 1
t += 1
return sb
var _threads = null # Array<InkThread>
var _thread_counter = 0 # int
var _start_of_root = InkPointer.null_pointer # Pointer
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type):
return type == "CallStack" || super.is_ink_class(type)
func get_ink_class():
return "CallStack"
# C# Actions & Delegates ##################################################### #
# (SimpleJson.Writer) -> void
func _anonymous_write_json(writer: InkSimpleJSON.Writer) -> void:
writer.write_property_start("threads")
writer.write_array_start()
for thread in self._threads:
thread.write_json(writer)
writer.write_array_end()
writer.write_property_end()
writer.write_property_start("threadCounter")
writer.write(self._thread_counter)
writer.write_property_end()
# ######################################################################## #
var StaticJSON: InkStaticJSON:
get: return _static_json.get_ref()
var _static_json = WeakRef.new()
func get_static_json(static_json = null):
if static_json != null:
_static_json = weakref(static_json)
return
var InkRuntime = Engine.get_main_loop().root.get_node("__InkRuntime")
InkUtils.__assert__(InkRuntime != null,
str("[InkCallStack] Could not retrieve 'InkRuntime' singleton from the scene tree."))
_static_json = weakref(InkRuntime.json)

View file

@ -0,0 +1,24 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
class_name Ink
# ############################################################################ #
enum ErrorType {
AUTHOR,
WARNING,
ERROR
}
enum PushPopType {
TUNNEL,
FUNCTION,
FUNCTION_EVALUATION_FROM_GAME
}

View file

@ -0,0 +1,31 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends RefCounted
class_name InkBase
# ############################################################################ #
func equals(_ink_base: InkBase) -> bool:
return false
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkBase"
func get_ink_class() -> String:
return "InkBase"

View file

@ -0,0 +1,212 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkObject
# ############################################################################ #
# Encapsulating parent into a weak ref.
var parent: InkObject:
get:
return self._parent.get_ref()
set(value):
self._parent = weakref(value)
var _parent: WeakRef = WeakRef.new() # InkObject
# ############################################################################ #
var debug_metadata: InkDebugMetadata:
get:
if _debug_metadata == null:
if self.parent:
return self.parent.debug_metadata
return _debug_metadata
set(value):
_debug_metadata = value
var _debug_metadata: InkDebugMetadata = null
# ############################################################################ #
var own_debug_metadata: InkDebugMetadata:
get: return _debug_metadata
# ############################################################################ #
# (InkPath) -> int?
func debug_line_number_of_path(path: InkPath):
if path == null:
return null
var root = self.root_content_container
if root != null:
var target_content := root.content_at_path(path).obj
if target_content:
var dm = target_content.debug_metadata
if dm != null:
return dm.start_line_number
return null
# TODO: Make inspectable
# InkPath
var path: InkPath:
get:
if _path == null:
if self.parent == null:
_path = InkPath.new()
else:
var comps: Array = [] # Stack<Path3D.Component>
var child = self
var container = InkUtils.as_or_null(child.parent, "InkContainer")
while container:
var named_child = InkUtils.as_INamedContent_or_null(child)
if (named_child != null && named_child.has_valid_name):
comps.push_front(InkPath.Component.new(named_child.name))
else:
comps.push_front(InkPath.Component.new(container.content.find(child)))
child = container
container = InkUtils.as_or_null(container.parent, "InkContainer")
_path = InkPath.new_with_components(comps)
return _path
var _path: InkPath = null
func resolve_path(path: InkPath) -> InkSearchResult:
if path.is_relative:
var nearest_container = InkUtils.as_or_null(self, "InkContainer")
if !nearest_container:
InkUtils.__assert__(
self.parent != null,
"Can't resolve relative path because we don't have a parent"
)
nearest_container = InkUtils.as_or_null(self.parent, "InkContainer")
InkUtils.__assert__(nearest_container != null, "Expected parent to be a container")
InkUtils.__assert__(path.get_component(0).is_parent)
path = path.tail
return nearest_container.content_at_path(path)
else:
return self.root_content_container.content_at_path(path)
func convert_path_to_relative(global_path: InkPath) -> InkPath:
var own_path := self.path
var min_path_length: int = min(global_path.length, own_path.length)
var last_shared_path_comp_index: int = -1
var i: int = 0
while i < min_path_length:
var own_comp: InkPath.Component = own_path.get_component(i)
var other_comp: InkPath.Component = global_path.get_component(i)
if own_comp.equals(other_comp):
last_shared_path_comp_index = i
else:
break
i += 1
if last_shared_path_comp_index == -1:
return global_path
var num_upwards_moves: int = (own_path.length - 1) - last_shared_path_comp_index
var new_path_comps: Array = [] # Array<InkPath.Component>
var up = 0
while up < num_upwards_moves:
new_path_comps.append(InkPath.Component.to_parent())
up += 1
var down = last_shared_path_comp_index + 1
while down < global_path.length:
new_path_comps.append(global_path.get_component(down))
down += 1
var relative_path = InkPath.new_with_components(new_path_comps, true)
return relative_path
func compact_path_string(other_path: InkPath) -> String:
var global_path_str
var relative_path_str
if other_path.is_relative:
relative_path_str = other_path.components_string
global_path_str = self.path.path_by_appending_path(other_path).components_string
else:
var relative_path = convert_path_to_relative(other_path)
relative_path_str = relative_path.components_string
global_path_str = other_path.components_string
if (relative_path_str.length() < global_path_str.length()):
return relative_path_str
else:
return global_path_str
# () -> InkContainer
var root_content_container: InkContainer:
get:
var ancestor := self
while (ancestor.parent):
ancestor = ancestor.parent
return InkUtils.as_or_null(ancestor, "InkContainer")
# () -> InkObject
func copy():
InkUtils.throw_exception("Not Implemented: Doesn't support copying")
return null
# (InkObject, InkObject) -> void
func set_child(obj: InkObject, value: InkObject):
if obj:
obj.parent = null
obj = value
if obj:
obj.parent = self
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkObject" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkObject"

View file

@ -0,0 +1,58 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkChoice
# ############################################################################ #
var text: String
var path_string_on_choice: String:
get:
# TODO: handle null case?
return target_path._to_string()
set(value):
target_path = InkPath.new_with_components_string(value)
var source_path = null # String?
var index: int = 0
var target_path: InkPath = null
var thread_at_generation: InkCallStack.InkThread = null
var original_thread_index: int = 0
var is_invisible_default: bool = false
var tags = null # Array<String>?
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type):
return type == "Choice" || super.is_ink_class(type)
func get_ink_class():
return "Choice"

View file

@ -0,0 +1,122 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkChoicePoint
# ############################################################################ #
# () -> InkPath
# (InkPath) -> void
var path_on_choice: InkPath:
get:
if self._path_on_choice != null && self._path_on_choice.is_relative:
var choice_target_obj := self.choice_target
if choice_target_obj:
self._path_on_choice = choice_target_obj.path
return _path_on_choice
set(value):
_path_on_choice = value
var _path_on_choice: InkPath = null
# ############################################################################ #
var choice_target: InkContainer:
get:
var cont: InkContainer = resolve_path(self._path_on_choice).container
return cont
# ############################################################################ #
var path_string_on_choice: String:
get:
return compact_path_string(self.path_on_choice)
set(value):
self.path_on_choice = InkPath.new_with_components_string(value)
# ############################################################################ #
var has_condition: bool
var has_start_content: bool
var has_choice_only_content: bool
var once_only: bool
var is_invisible_default: bool
# ############################################################################ #
var flags: int:
get:
var flags: int = 0
if has_condition:
flags |= 1
if has_start_content:
flags |= 2
if has_choice_only_content:
flags |= 4
if is_invisible_default:
flags |= 8
if once_only:
flags |= 16
return flags
set(value):
has_condition = (value & 1) > 0
has_start_content = (value & 2) > 0
has_choice_only_content = (value & 4) > 0
is_invisible_default = (value & 8) > 0
once_only = (value & 16) > 0
# ############################################################################ #
func _init(once_only: bool = true):
self.once_only = once_only
func _to_string() -> String:
var target_line_num = debug_line_number_of_path(self.path_on_choice)
var target_string := self.path_on_choice._to_string()
if target_line_num != null:
target_string = " line %d(%s)" % [target_line_num, target_string]
return "Choice: -> %s" % target_string
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "ChoicePoint" || super.is_ink_class(type)
func get_ink_class() -> String:
return "ChoicePoint"

View file

@ -0,0 +1,333 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkContainer
# ############################################################################ #
var name = null # String?
var content: Array: # Array<InkObject>
get:
return self._content
set(value):
add_content(value)
var _content: Array # Array<InkObject>
var named_content: Dictionary # Dictionary<string, INamedContent>
var named_only_content: # Dictionary<string, InkObject>?
get:
var named_only_content_dict = {} # Dictionary<string, InkObject>?
for key in self.named_content:
named_only_content_dict[key] = self.named_content[key]
for c in self.content:
var named = InkUtils.as_INamedContent_or_null(c)
if named != null && named.has_valid_name:
named_only_content_dict.erase(named.name)
if named_only_content_dict.size() == 0:
named_only_content_dict = null
return named_only_content_dict
set(value):
var existing_named_only = named_only_content
if existing_named_only != null:
for key in existing_named_only:
self.named_content.erase(key)
if value == null:
return
for key in value:
var named = InkUtils.as_INamedContent_or_null(value[key])
if named != null:
add_to_named_content_only(named)
var visits_should_be_counted: bool = false
var turn_index_should_be_counted: bool = false
var counting_at_start_only: bool = false
enum CountFlags {
VISITS = 1,
TURNS = 2,
COUNT_START_ONLY = 4
}
# CountFlags
var count_flags: int:
get:
var flags = 0
if visits_should_be_counted: flags |= CountFlags.VISITS
if turn_index_should_be_counted: flags |= CountFlags.TURNS
if counting_at_start_only: flags |= CountFlags.COUNT_START_ONLY
if flags == CountFlags.COUNT_START_ONLY:
flags = 0
return flags
set(value):
var flag = value
if (flag & CountFlags.VISITS) > 0: visits_should_be_counted = true
if (flag & CountFlags.TURNS) > 0: turn_index_should_be_counted = true
if (flag & CountFlags.COUNT_START_ONLY) > 0: counting_at_start_only = true
var has_valid_name: bool:
get: return self.name != null && self.name.length() > 0
var path_to_first_leaf_content: InkPath:
get:
if self._path_to_first_leaf_content == null:
self._path_to_first_leaf_content = self.path.path_by_appending_path(self.internal_path_to_first_leaf_content)
return self._path_to_first_leaf_content
# InkPath?
var _path_to_first_leaf_content: InkPath = null
# TODO: Make inspectable
var internal_path_to_first_leaf_content: InkPath:
get:
var components: Array = [] # Array<InkPath.InkComponent>
var container: InkContainer = self
while container != null:
if container.content.size() > 0:
components.append(InkPath.Component.new(0))
container = InkUtils.as_or_null(container.content[0], "InkContainer")
return InkPath.new_with_components(components)
func _init():
self._content = [] # Array<InkObject>
self.named_content = {} # Dictionary<string, INamedContent>
func add_content(content_obj_or_content_list) -> void:
if InkUtils.is_ink_class(content_obj_or_content_list, "InkObject"):
var content_obj: InkObject = content_obj_or_content_list
self.content.append(content_obj)
if content_obj.parent:
InkUtils.throw_exception("content is already in %s" % content_obj.parent._to_string())
return
content_obj.parent = self
try_add_named_content(content_obj)
elif content_obj_or_content_list is Array:
var content_list: Array = content_obj_or_content_list
for c in content_list:
add_content(c)
func insert_content(content_obj: InkObject, index: int) -> void:
self.content.insert(index, content_obj)
if content_obj.parent:
InkUtils.throw_exception("content is already in %s" % content_obj.parent._to_string())
return
content_obj.parent = self
try_add_named_content(content_obj)
func try_add_named_content(content_obj: InkObject) -> void:
var named_content_obj = InkUtils.as_INamedContent_or_null(content_obj)
if (named_content_obj != null && named_content_obj.has_valid_name):
add_to_named_content_only(named_content_obj)
# (INamedContent) -> void
func add_to_named_content_only(named_content_obj: InkObject) -> void:
InkUtils.__assert__(named_content_obj.is_ink_class("InkObject"), "Can only add Runtime.Objects to a Runtime.Container")
var runtime_obj = named_content_obj
runtime_obj.parent = self
named_content[named_content_obj.name] = named_content_obj
func add_contents_of_container(other_container: InkContainer) -> void:
self.content = self.content + other_container.content
for obj in other_container.content:
obj.parent = self
try_add_named_content(obj)
func content_with_path_component(component: InkPath.Component) -> InkObject:
if component.is_index:
if component.index >= 0 && component.index < self.content.size():
return self.content[component.index]
else:
return null
elif component.is_parent:
return self.parent
else:
if named_content.has(component.name):
var found_content = named_content[component.name]
return found_content
else:
return null
func content_at_path(
path: InkPath,
partial_path_start: int = 0,
partial_path_length: int = -1
) -> InkSearchResult:
if partial_path_length == -1:
partial_path_length = path.length
var result: InkSearchResult = InkSearchResult.new()
result.approximate = false
var current_container: InkContainer = self
var current_obj: InkObject = self
var i: int = partial_path_start
while i < partial_path_length:
var comp = path.get_component(i)
if current_container == null:
result.approximate = true
break
var found_obj: InkObject = current_container.content_with_path_component(comp)
if found_obj == null:
result.approximate = true
break
current_obj = found_obj
current_container = InkUtils.as_or_null(found_obj, "InkContainer")
i += 1
result.obj = current_obj
return result
func build_string_of_hierarchy(
existing_hierarchy: String,
indentation: int,
pointed_obj: InkObject
) -> String:
existing_hierarchy = _append_indentation(existing_hierarchy, indentation)
existing_hierarchy += "["
if self.has_valid_name:
existing_hierarchy += str(" (%s) " % self.name)
if self == pointed_obj:
existing_hierarchy += " <---"
existing_hierarchy += "\n"
indentation += 1
var i = 0
while i < self.content.size():
var obj = self.content[i]
if InkUtils.is_ink_class(obj, "InkContainer"):
existing_hierarchy = obj.build_string_of_hierarchy(existing_hierarchy, indentation, pointed_obj)
else:
existing_hierarchy = _append_indentation(existing_hierarchy, indentation)
if InkUtils.is_ink_class(obj, "StringValue"):
existing_hierarchy += "\""
existing_hierarchy += obj._to_string().replace("\n", "\\n")
existing_hierarchy += "\""
else:
existing_hierarchy += obj._to_string()
if i != self.content.size() - 1:
existing_hierarchy += ","
if !InkUtils.is_ink_class(obj, "InkContainer") && obj == pointed_obj:
existing_hierarchy += " <---"
existing_hierarchy += "\n"
i += 1
var only_named: Dictionary = {} # Dictionary<String, INamedContent>
for obj_key in self.named_content:
var value = self.named_content[obj_key]
if self.content.find(value) != -1:
continue
else:
only_named[obj_key] = value
if only_named.size() > 0:
existing_hierarchy = _append_indentation(existing_hierarchy, indentation)
existing_hierarchy += "-- named: --\n"
for object_key in only_named:
var value = only_named[object_key]
InkUtils.__assert__(InkUtils.is_ink_class(value, "InkContainer"), "Can only print out named Containers")
var container = value
existing_hierarchy = container.build_string_of_hierarchy(existing_hierarchy, indentation, pointed_obj)
existing_hierarchy += "\n"
indentation -= 1
existing_hierarchy = _append_indentation(existing_hierarchy, indentation)
existing_hierarchy += "]"
return existing_hierarchy
func build_full_string_of_hierarchy() -> String:
return build_string_of_hierarchy("", 0, null)
func _append_indentation(string: String, indentation: int) -> String:
var spaces_per_indent = 4
var i = 0
while(i < spaces_per_indent * indentation):
string += " "
i += 1
return string
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkContainer" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkContainer"

View file

@ -0,0 +1,213 @@
# warning-ignore-all:shadowed_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkControlCommand
# ############################################################################ #
enum CommandType {
NOT_SET = -1,
EVAL_START,
EVAL_OUTPUT,
EVAL_END,
DUPLICATE,
POP_EVALUATED_VALUE,
POP_FUNCTION,
POP_TUNNEL,
BEGIN_STRING,
END_STRING,
NO_OP,
CHOICE_COUNT,
TURNS,
TURNS_SINCE,
READ_COUNT,
RANDOM,
SEED_RANDOM,
VISIT_INDEX,
SEQUENCE_SHUFFLE_INDEX,
START_THREAD,
DONE,
END,
LIST_FROM_INT,
LIST_RANGE,
LIST_RANDOM,
BEGIN_TAG,
END_TAG,
#----
TOTAL_VALUES
}
# ############################################################################ #
# CommandType
var command_type: int
# ############################################################################ #
@warning_ignore("shadowed_variable")
func _init(command_type: int = CommandType.NOT_SET):
self.command_type = command_type
# ############################################################################ #
func copy() -> InkControlCommand:
return InkControlCommand.new(self.command_type)
static func eval_start() -> InkControlCommand:
return InkControlCommand.new(CommandType.EVAL_START)
static func eval_output() -> InkControlCommand:
return InkControlCommand.new(CommandType.EVAL_OUTPUT)
static func eval_end() -> InkControlCommand:
return InkControlCommand.new(CommandType.EVAL_END)
static func duplicate() -> InkControlCommand:
return InkControlCommand.new(CommandType.DUPLICATE)
static func pop_evaluated_value() -> InkControlCommand:
return InkControlCommand.new(CommandType.POP_EVALUATED_VALUE)
static func pop_function() -> InkControlCommand:
return InkControlCommand.new(CommandType.POP_FUNCTION)
static func pop_tunnel() -> InkControlCommand:
return InkControlCommand.new(CommandType.POP_TUNNEL)
static func begin_string() -> InkControlCommand:
return InkControlCommand.new(CommandType.BEGIN_STRING)
static func end_string() -> InkControlCommand:
return InkControlCommand.new(CommandType.END_STRING)
static func no_op() -> InkControlCommand:
return InkControlCommand.new(CommandType.NO_OP)
static func choice_count() -> InkControlCommand:
return InkControlCommand.new(CommandType.CHOICE_COUNT)
static func turns() -> InkControlCommand:
return InkControlCommand.new(CommandType.TURNS)
static func turns_since() -> InkControlCommand:
return InkControlCommand.new(CommandType.TURNS_SINCE)
static func read_count() -> InkControlCommand:
return InkControlCommand.new(CommandType.READ_COUNT)
static func random() -> InkControlCommand:
return InkControlCommand.new(CommandType.RANDOM)
static func seed_random() -> InkControlCommand:
return InkControlCommand.new(CommandType.SEED_RANDOM)
static func visit_index() -> InkControlCommand:
return InkControlCommand.new(CommandType.VISIT_INDEX)
static func sequence_shuffle_index() -> InkControlCommand:
return InkControlCommand.new(CommandType.SEQUENCE_SHUFFLE_INDEX)
static func done() -> InkControlCommand:
return InkControlCommand.new(CommandType.DONE)
static func end() -> InkControlCommand:
return InkControlCommand.new(CommandType.END)
static func list_from_int() -> InkControlCommand:
return InkControlCommand.new(CommandType.LIST_FROM_INT)
static func list_range() -> InkControlCommand:
return InkControlCommand.new(CommandType.LIST_RANGE)
static func list_random() -> InkControlCommand:
return InkControlCommand.new(CommandType.LIST_RANDOM)
static func begin_tag() -> InkControlCommand:
return InkControlCommand.new(CommandType.BEGIN_TAG)
static func end_tag() -> InkControlCommand:
return InkControlCommand.new(CommandType.END_TAG)
# () -> String
func _to_string() -> String:
var command_name: String = ""
match self.command_type:
CommandType.NOT_SET: command_name = "NOT_SET"
CommandType.EVAL_START: command_name = "EVAL_START"
CommandType.EVAL_OUTPUT: command_name = "EVAL_OUTPUT"
CommandType.EVAL_END: command_name = "EVAL_END"
CommandType.DUPLICATE: command_name = "DUPLICATE"
CommandType.POP_EVALUATED_VALUE: command_name = "POP_EVALUATED_VALUE"
CommandType.POP_FUNCTION: command_name = "POP_FUNCTION"
CommandType.POP_TUNNEL: command_name = "POP_TUNNEL"
CommandType.BEGIN_STRING: command_name = "BEGIN_STRING"
CommandType.END_STRING: command_name = "END_STRING"
CommandType.NO_OP: command_name = "NO_OP"
CommandType.CHOICE_COUNT: command_name = "CHOICE_COUNT"
CommandType.TURNS: command_name = "TURNS"
CommandType.TURNS_SINCE: command_name = "TURNS_SINCE"
CommandType.READ_COUNT: command_name = "READ_COUNT"
CommandType.RANDOM: command_name = "RANDOM"
CommandType.SEED_RANDOM: command_name = "SEED_RANDOM"
CommandType.VISIT_INDEX: command_name = "VISIT_INDEX"
CommandType.SEQUENCE_SHUFFLE_INDEX: command_name = "SEQUENCE_SHUFFLE_INDEX"
CommandType.START_THREAD: command_name = "START_THREAD"
CommandType.DONE: command_name = "DONE"
CommandType.END: command_name = "END"
CommandType.LIST_FROM_INT: command_name = "LIST_FROM_INT"
CommandType.LIST_RANGE: command_name = "LIST_RANGE"
CommandType.LIST_RANDOM: command_name = "LIST_RANDOM"
CommandType.BEGIN_TAG: command_name = "BEGIN_TAG"
CommandType.END_TAG: command_name = "END_TAG"
CommandType.TOTAL_VALUES: command_name = "TOTAL_VALUES"
return "Command(%s)" % command_name
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "ControlCommand" || super.is_ink_class(type)
func get_ink_class() -> String:
return "ControlCommand"

View file

@ -0,0 +1,145 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkDivert
# ############################################################################ #
var target_path: InkPath:
get:
if self._target_path != null && self._target_path.is_relative:
var target_obj: InkObject = self.target_pointer.resolve()
if target_obj:
self._target_path = target_obj.path
return self._target_path
set(value):
self._target_path = value
self._target_pointer = InkPointer.null_pointer
var _target_path: InkPath = null
var target_pointer: InkPointer:
get:
if self._target_pointer.is_null:
var target_obj = resolve_path(self._target_path).obj
if self._target_path.last_component.is_index:
self._target_pointer = InkPointer.new(
InkUtils.as_or_null(target_obj.parent, "InkContainer"),
self._target_path.last_component.index
)
else:
self._target_pointer = InkPointer.start_of(InkUtils.as_or_null(target_obj, "InkContainer"))
return self._target_pointer
var _target_pointer: InkPointer = InkPointer.null_pointer
var target_path_string: # String?
get:
if self.target_path == null:
return null
return self.compact_path_string(self.target_path)
set(value):
if value == null:
self.target_path = null
else:
self.target_path = InkPath.new_with_components_string(value)
var variable_divert_name = null # String?
var has_variable_target: bool:
get: return self.variable_divert_name != null
var pushes_to_stack: bool = false
var stack_push_type: int = 0 # Ink.PushPopType
var is_external: bool = false
var external_args: int = 0
var is_conditional: bool = false
# (int?) -> InkDivert
@warning_ignore("shadowed_variable")
func _init_with(stack_push_type = null):
self.pushes_to_stack = false
if stack_push_type != null:
self.pushes_to_stack = true
self.stack_push_type = stack_push_type
func equals(obj: InkBase) -> bool:
var other_divert: InkDivert = InkUtils.as_or_null(obj, "Divert")
if other_divert:
if self.has_variable_target == other_divert.has_variable_target:
if self.has_variable_target:
return self.variable_divert_name == other_divert.variable_divert_name
else:
return self.target_path.equals(other_divert.target_path)
return false
func _to_string() -> String:
if self.has_variable_target:
return "Divert(variable: %s)" % self.variable_divert_name
elif self.target_path == null:
return "Divert(null)"
else:
var _string = ""
var target_str: String = self.target_path._to_string()
var target_line_num = debug_line_number_of_path(self.target_path)
if target_line_num != null:
target_str = "line " + target_line_num
_string += "Divert"
if self.is_conditional:
_string += "?"
if self.pushes_to_stack:
if self.stack_push_type == Ink.PushPopType.FUNCTION:
_string += " function"
else:
_string += " tunnel"
_string += " -> "
_string += self.target_path_string
_string += " (%s)" % target_str
return _string
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "Divert" || super.is_ink_class(type)
func get_ink_class() -> String:
return "Divert"

View file

@ -0,0 +1,29 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkGlue
# ############################################################################ #
func _to_string() -> String:
return "Glue"
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "Glue" || super.is_ink_class(type)
func get_ink_class() -> String:
return "Glue"

View file

@ -0,0 +1,372 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkNativeFunctionCall
# ############################################################################ #
# Imports
# ############################################################################ #
# TODO: Migrate to Ink.ValueType
const ValueType = preload("res://addons/inkgd/runtime/values/value_type.gd").ValueType
# ############################################################################ #
# (String) -> NativeFunctionCall
@warning_ignore("shadowed_variable")
static func call_with_name(
function_name: String,
_static_native_function_call: InkStaticNativeFunctionCall = null
) -> InkNativeFunctionCall:
return InkNativeFunctionCall.new_with_name(function_name, _static_native_function_call)
var name: String:
get:
return _name
set(value):
_name = value
if !_is_prototype:
_prototype = self._static_native_function_call.native_functions[_name]
var _name: String
var number_of_parameters: int:
get:
if _prototype:
return _prototype.number_of_parameters
else:
return _number_of_parameters
set(value):
_number_of_parameters = value
var _number_of_parameters: int = 0
# (Array<InkObject>) -> InkObject
#
# The name is different to avoid shadowing 'Object.call'
#
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func call_with_parameters(parameters: Array, metadata: StoryErrorMetadata) -> InkObject:
if _prototype:
return _prototype.call_with_parameters(parameters, metadata)
if self.number_of_parameters != parameters.size():
InkUtils.throw_exception("Unexpected number of parameters")
return null
var has_list = false
for p in parameters:
if InkUtils.is_ink_class(p, "Void"):
InkUtils.throw_story_exception(
"Attempting to perform operation on a void value. Did you forget to " +
"'return' a value from a function you called here?",
false,
metadata
)
return null
if InkUtils.is_ink_class(p, "ListValue"):
has_list = true
if parameters.size() == 2 && has_list:
return call_binary_list_operation(parameters, metadata)
var coerced_params: Array = coerce_values_to_single_type(parameters, metadata)
# ValueType
var coerced_type: int = coerced_params[0].value_type
if (
coerced_type == ValueType.INT ||
coerced_type == ValueType.FLOAT ||
coerced_type == ValueType.STRING ||
coerced_type == ValueType.DIVERT_TARGET ||
coerced_type == ValueType.LIST
):
return call_coerced(coerced_params, metadata)
return null
# (Array<Value>) -> Value # Call<T> in the original code
#
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func call_coerced(parameters_of_single_type: Array, metadata: StoryErrorMetadata) -> InkValue:
var param1: InkValue = parameters_of_single_type[0]
var val_type: int = param1.value_type
var param_count: int = parameters_of_single_type.size()
if param_count == 2 || param_count == 1:
var op_for_type = null
if _operation_funcs.has(val_type):
op_for_type = _operation_funcs[val_type]
else:
var type_name = InkUtils.value_type_name(val_type)
InkUtils.throw_story_exception(
"Cannot perform operation '%s' on value of type (%d)" \
% [self.name, type_name],
false,
metadata
)
return null
if param_count == 2:
var param2 = parameters_of_single_type[1]
var result_val = self._static_native_function_call.call(op_for_type, param1.value, param2.value)
return InkValue.create(result_val)
else:
var result_val = self._static_native_function_call.call(op_for_type, param1.value)
return InkValue.create(result_val)
else:
InkUtils.throw_exception(
"Unexpected number of parameters to NativeFunctionCall: %d" % \
parameters_of_single_type.size()
)
return null
# (Array<InkObject>) -> Value
#
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func call_binary_list_operation(parameters: Array, metadata: StoryErrorMetadata) -> InkValue:
if ((self.name == "+" || self.name == "-") &&
InkUtils.is_ink_class(parameters[0], "ListValue") &&
InkUtils.is_ink_class(parameters [1], "IntValue")
):
return call_list_increment_operation(parameters)
var v1 = InkUtils.as_or_null(parameters[0], "Value")
var v2 = InkUtils.as_or_null(parameters[1], "Value")
if ((self.name == "&&" || self.name == "||") &&
(v1.value_type != ValueType.LIST || v2.value_type != ValueType.LIST)
):
var op: String = _operation_funcs[ValueType.INT]
var result = bool(self._static_native_function_call.call(
"op_for_type",
1 if v1.is_truthy else 0,
1 if v2.is_truthy else 0
))
return InkBoolValue.new_with(result)
if v1.value_type == ValueType.LIST && v2.value_type == ValueType.LIST:
return call_coerced([v1, v2], metadata)
var v1_type_name = InkUtils.value_type_name(v1.value_type)
var v2_type_name = InkUtils.value_type_name(v2.value_type)
InkUtils.throw_story_exception(
"Can not call use '%s' operation on %s and %s" % \
[self.name, v1_type_name, v2_type_name],
false,
metadata
)
return null
# (Array<InkObject>) -> Value
func call_list_increment_operation(list_int_params: Array) -> InkValue:
var list_val: InkListValue = InkUtils.cast(list_int_params[0], "ListValue")
var int_val: InkIntValue = InkUtils.cast(list_int_params [1], "IntValue")
var result_raw_list = InkList.new()
for list_item in list_val.value.keys(): # TODO: Optimize?
var list_item_value = list_val.value.get_item(list_item)
var int_op: String = _operation_funcs[ValueType.INT]
var target_int = int(
self._static_native_function_call.call(
int_op,
list_item_value,
int_val.value
)
)
var item_origin: InkListDefinition = null
for origin in list_val.value.origins:
if origin.name == list_item.origin_name:
item_origin = origin
break
if item_origin != null:
var incremented_item: InkTryGetResult = item_origin.try_get_item_with_value(target_int)
if incremented_item.exists:
result_raw_list.set_item(incremented_item.result, target_int)
return InkListValue.new_with(result_raw_list)
# (Array<InkObject>) -> Array<Value>?
#
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadata are used in case an 'exception'
# is raised. For more information, see story.gd.
func coerce_values_to_single_type(parameters_in: Array, metadata: StoryErrorMetadata):
var val_type: int = ValueType.INT
var special_case_list: InkListValue = null
for obj in parameters_in:
var val: InkValue = obj
if val.value_type > val_type:
val_type = val.value_type
if val.value_type == ValueType.LIST:
special_case_list = InkUtils.as_or_null(val, "ListValue")
var parameters_out: Array = [] # Array<Value>
if val_type == ValueType.LIST:
for val in parameters_in:
if val.value_type == ValueType.LIST:
parameters_out.append(val)
elif val.value_type == ValueType.INT:
var int_val = int(val.value_object)
var list = special_case_list.value.origin_of_max_item
var item: InkTryGetResult = list.try_get_item_with_value(int_val)
if item.exists:
var casted_value = InkListValue.new_with_single_item(item.result, int_val)
parameters_out.append(casted_value)
else:
InkUtils.throw_story_exception(
"Could not find List item with the value %d in %s" \
% [int_val, list.name],
false,
metadata
)
return null
else:
var type_name = InkUtils.value_type_name(val.value_type)
InkUtils.throw_story_exception(
"Cannot mix Lists and %s values in this operation" % type_name,
false,
metadata
)
return null
else:
for val in parameters_in:
var casted_value = val.cast(val_type)
parameters_out.append(casted_value)
return parameters_out
func _init(static_native_function_call: InkStaticNativeFunctionCall = null):
generate_native_functions_if_necessary(static_native_function_call)
@warning_ignore("shadowed_variable")
func _init_with_name(name: String):
self.name = name
@warning_ignore("shadowed_variable")
func _init_with_name_and_number_of_parameters(name: String, number_of_parameters: int):
_is_prototype = true
self.name = name
self.number_of_parameters = number_of_parameters
func generate_native_functions_if_necessary(static_native_function_call: InkStaticNativeFunctionCall) -> void:
find_static_objects(static_native_function_call)
self._static_native_function_call.generate_native_functions_if_necessary()
func add_op_func_for_type(val_type: int, op: String) -> void:
if _operation_funcs == null:
_operation_funcs = {}
_operation_funcs[val_type] = op
func _to_string() -> String:
return "Native '%s'" % self.name
var _prototype: InkNativeFunctionCall = null
var _is_prototype: bool = false
# Dictionary<ValueType, String>
var _operation_funcs: Dictionary = {}
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type):
return type == "NativeFunctionCall" || super.is_ink_class(type)
func get_ink_class():
return "NativeFunctionCall"
var _static_native_function_call: InkStaticNativeFunctionCall:
get: return _weak_static_native_function_call.get_ref()
var _weak_static_native_function_call = WeakRef.new()
func find_static_objects(static_native_function_call: InkStaticNativeFunctionCall = null):
if _static_native_function_call == null:
if static_native_function_call:
_weak_static_native_function_call = weakref(static_native_function_call)
else:
var ink_runtime = Engine.get_main_loop().root.get_node("__InkRuntime")
_weak_static_native_function_call = weakref(ink_runtime.native_function_call)
# ############################################################################ #
@warning_ignore("shadowed_variable")
static func new_with_name(
name: String,
static_native_function_call: InkStaticNativeFunctionCall = null
):
var native_function_call = InkNativeFunctionCall.new(static_native_function_call)
native_function_call._init_with_name(name)
return native_function_call
@warning_ignore("shadowed_variable")
static func new_with_name_and_number_of_parameters(
name: String,
number_of_parameters: int,
static_native_function_call: InkStaticNativeFunctionCall = null
):
var native_function_call = InkNativeFunctionCall.new(static_native_function_call)
native_function_call._init_with_name_and_number_of_parameters(name, number_of_parameters)
return native_function_call

View file

@ -0,0 +1,36 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkTag
# ############################################################################ #
var text: String
func _init(tag_text: String):
text = tag_text
func _to_string() -> String:
return '# ' + text
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "Tag" || super.is_ink_class(type)
func get_ink_class() -> String:
return "Tag"

View file

@ -0,0 +1,60 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkVariableAssignment
# ############################################################################ #
var variable_name = null # String?
var is_new_declaration: bool = false
var is_global: bool = false
func _init():
_init_with(null, false)
# (String?, bool) -> InkVariableAssignment
@warning_ignore("shadowed_variable")
func _init_with(variable_name, is_new_declaration: bool):
self.variable_name = variable_name
self.is_new_declaration = is_new_declaration
func _to_string() -> String:
return "VarAssign to %s" % variable_name
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "VariableAssignment" || super.is_ink_class(type)
func get_ink_class() -> String:
return "VariableAssignment"
# (String?, bool) -> InkVariableAssignment
@warning_ignore("shadowed_variable")
static func new_with(
variable_name: String,
is_new_declaration: bool
) -> InkVariableAssignment:
var variable_assignment = InkVariableAssignment.new()
variable_assignment._init_with(variable_name, is_new_declaration)
return variable_assignment

View file

@ -0,0 +1,69 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkVariableReference
# ############################################################################ #
var name = null # String?
# InkPath
var path_for_count: InkPath = null
# Container?
var container_for_count: InkContainer:
get: return self.resolve_path(path_for_count).container
# String?
var path_string_for_count:
get:
if path_for_count == null:
return null
return compact_path_string(path_for_count)
set(value):
if value == null:
path_for_count = null
else:
path_for_count = InkPath.new_with_components_string(value)
# ############################################################################ #
@warning_ignore("shadowed_variable")
func _init(name = null):
if name:
self.name = name
# ############################################################################ #
func _to_string() -> String:
if name != null:
return "var(%s)" % name
else:
var path_str = self.path_string_for_count
return "read_count(%s)" % path_str
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "VariableReference" || super.is_ink_class(type)
func get_ink_class() -> String:
return "VariableReference"

View file

@ -0,0 +1,27 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkVoid
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "Void" || super.is_ink_class(type)
func get_ink_class() -> String:
return "Void"
func _to_string() -> String:
return "Void"

View file

@ -0,0 +1,76 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkDebugMetadata
# ############################################################################ #
var start_line_number: int = 0
var end_line_number: int = 0
var start_character_number: int = 0
var end_character_number: int = 0
# String?
var file_name = null
# String?
var source_name = null
# ############################################################################ #
func merge(dm: InkDebugMetadata) -> InkDebugMetadata:
var new_debug_metadata = DebugMetadata().new()
new_debug_metadata.file_name = self.file_name
new_debug_metadata.source_name = self.source_name
if self.start_line_number < dm.start_line_number:
new_debug_metadata.start_line_number = self.start_line_number
new_debug_metadata.start_character_number = self.start_character_number
elif self.start_line_number > dm.start_line_number:
new_debug_metadata.start_line_number = dm.start_line_number
new_debug_metadata.start_character_number = dm.start_character_number
else:
var min_scn = min(self.start_character_number, dm.start_character_number)
new_debug_metadata.start_line_number = self.start_line_number
new_debug_metadata.start_character_number = min_scn
if self.end_line_number > dm.end_line_number:
new_debug_metadata.end_line_number = self.end_line_number
new_debug_metadata.end_character_number = self.end_character_number
elif self.end_line_number < dm.end_line_number:
new_debug_metadata.end_line_number = dm.end_line_number
new_debug_metadata.end_character_number = dm.end_character_number
else:
var max_scn = min(self.end_character_number, dm.end_character_number)
new_debug_metadata.end_line_number = self.end_line_number
new_debug_metadata.end_character_number = max_scn
return new_debug_metadata
# () -> String
func _to_string() -> String:
if file_name != null:
return str("line ", start_line_number, " of ", file_name)
else:
return str("line ", start_line_number)
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "DebugMetadata" || super.is_ink_class(type)
func get_ink_class() -> String:
return "DebugMetadata"
static func DebugMetadata():
return load("res://addons/inkgd/runtime/debug_metadata.gd")

View file

@ -0,0 +1,24 @@
# warning-ignore-all:shadowed_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends RefCounted
class_name InkFunctionResult
# ############################################################################ #
var text_output: String = ""
var return_value = null
# ############################################################################ #
func _init(text_output: String, return_value):
self.text_output = text_output
self.return_value = return_value

View file

@ -0,0 +1,39 @@
# warning-ignore-all:shadowed_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends RefCounted
class_name InkKeyValuePair
# ############################################################################ #
var key = null
var value = null
# ############################################################################ #
# TODO: Use _init instead of _init_with_key_value.
func _init():
pass
func _init_with_key_value(key, value):
self.key = key
self.value = value
func _to_string():
return ("[KeyValuePair (%s, %s)]" % [key, value])
# ############################################################################ #
static func new_with_key_value(key, value) -> InkKeyValuePair:
var key_value_pair = InkKeyValuePair.new()
key_value_pair._init_with_key_value(key, value)
return key_value_pair

View file

@ -0,0 +1,50 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# ############################################################################ #
# !! VALUE TYPE
# ############################################################################ #
# This element is only used during JSON parsing and is never duplicated / passed
# around so it doesn't need to be either immutable or have a 'duplicate' method.
class_name InkStateElement
# ############################################################################ #
enum State {
NONE,
OBJECT,
ARRAY,
PROPERTY,
PROPERTY_NAME,
STRING,
}
# ############################################################################ #
var type: int = State.NONE # State
var child_count: int = 0
# ############################################################################ #
func _init(type: int):
self.type = type
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type) -> bool:
return type == "StateElement"
func get_ink_class() -> String:
return "StateElement"

View file

@ -0,0 +1,34 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# Simple replacement of the Stopwatch class from the .NET Framework.
# Less accurate than the original implemntation, but good enough for
# the use-case.
class_name InkStopWatch
# ############################################################################ #
var _start_time: int = -1
var elapsed_milliseconds : get = get_elapsed_milliseconds
func get_elapsed_milliseconds() -> int:
if _start_time == -1:
return 0
return Time.get_ticks_msec() - _start_time
# ############################################################################ #
func start() -> void:
_start_time = Time.get_ticks_msec()
func stop() -> void:
_start_time = -1

View file

@ -0,0 +1,31 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# An object tha represents a "Story Error", which is equivalent in certain
# context to upstream's StoryException.
class_name StoryError
# ############################################################################ #
# Properties
# ############################################################################ #
var message: String
var use_end_line_number: bool
var metadata # StoryErrorMetadata | null
# ############################################################################ #
# Initialization
# ############################################################################ #
@warning_ignore("shadowed_variable")
func _init(message: String, use_end_line_number: bool, metadata):
self.message = message
self.use_end_line_number = use_end_line_number
self.metadata = metadata

View file

@ -0,0 +1,30 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# An object that keeps track of the Debug Metadata and current pointer at the
# exact moment an error was raised, so that they can be processed and reported
# later. It's required because GDScript doesn't support exceptions and
# errors don't bubble up the stack.
class_name StoryErrorMetadata
# ############################################################################ #
# Properties
# ############################################################################ #
var debug_metadata # InkDebugMetadata | null
var pointer: InkPointer
# ############################################################################ #
# Initialization
# ############################################################################ #
func _init(debug_metadata: InkDebugMetadata, pointer: InkPointer):
self.debug_metadata = debug_metadata
self.pointer = pointer

View file

@ -0,0 +1,63 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# Using an dictionary as the backing structure for a not-too-bad, super-simple
# set. The Ink runtime doesn't use C#'s HashSet full potential, so this trick
# should be good enough for the use-case.
# This simple set is designed to hold Strings only.
extends RefCounted
class_name InkStringSet
# ############################################################################ #
# Self-reference
# ############################################################################ #
static func InkStringSet() -> GDScript:
return load("res://addons/inkgd/runtime/extra/string_set.gd") as GDScript
# ############################################################################ #
var _dictionary: Dictionary = {}
# ############################################################################ #
func clear() -> void:
_dictionary.clear()
func duplicate() -> InkStringSet:
var set = InkStringSet().new()
set._dictionary = _dictionary.duplicate()
return set
func enumerate() -> Array:
return _dictionary.keys()
func is_empty() -> bool:
return _dictionary.is_empty()
func contains(element: String) -> bool:
return _dictionary.has(element)
func contains_all(elements: Array) -> bool:
return _dictionary.has_all(elements)
func size() -> int:
return _dictionary.size()
func to_array() -> Array:
return _dictionary.keys()
func append(value: String) -> void:
_dictionary[value] = null
func erase(value: String) -> bool:
return _dictionary.erase(value)

View file

@ -0,0 +1,33 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# Simple replacement of the StringWriter class from the .NET Framework.
# It has none of the optimisations of original class and merely wraps
# a plain old string.
class_name InkStringWriter
# ############################################################################ #
var _internal_string: String = ""
# ############################################################################ #
func _init():
pass
# ############################################################################ #
func write(s: String) -> void:
_internal_string += str(s)
func _to_string() -> String:
return _internal_string

View file

@ -0,0 +1,25 @@
# warning-ignore-all:shadowed_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends RefCounted
class_name InkTryGetResult
# ############################################################################ #
var exists: bool = false # Bool
var result = null # Variant
# ############################################################################ #
func _init(exists: bool, result):
self.exists = exists
self.result = result

View file

@ -0,0 +1,272 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends RefCounted
class_name InkUtils
# ############################################################################ #
# Imports
# ############################################################################ #
const ValueType = preload("res://addons/inkgd/runtime/values/value_type.gd").ValueType
# ############################################################################ #
# Exceptions
# ############################################################################ #
static func throw_exception(message: String) -> void:
InkRuntime().handle_exception(message)
static func throw_story_exception(
message: String,
use_end_line_number = false,
metadata = null
) -> void:
InkRuntime().handle_story_exception(message, use_end_line_number, metadata)
static func throw_argument_exception(message: String) -> void:
InkRuntime().handle_argument_exception(message)
# ############################################################################ #
# Assertions
# ############################################################################ #
static func __assert__(condition: bool, message = "") -> void:
if !condition && message != "":
printerr(message)
assert(condition)
# ############################################################################ #
# Type Assertion
# ############################################################################ #
static func as_or_null(variant, name_of_class: String):
if (
is_ink_class(variant, name_of_class) ||
(name_of_class == "Dictionary" && variant is Dictionary) ||
(name_of_class == "Array" && variant is Array)
):
return variant
else:
return null
static func cast(variant, name_of_class: String):
if is_ink_class(variant, name_of_class):
return variant
else:
push_error(
"Original implementation threw a RuntimeException here, because of a " +
"cast issue. Undefined behaviors should be expected."
)
assert(false)
return null
static func as_INamedContent_or_null(variant):
var properties = variant.get_property_list()
var has_has_valid_name = false
var has_name = false
for property in properties:
if property["name"] == "has_valid_name":
has_has_valid_name = true
if has_has_valid_name && has_name:
return variant
elif property["name"] == "name":
has_name = true
if has_has_valid_name && has_name:
return variant
return null
static func is_ink_class(object: Variant, name_of_class: String) -> bool:
return (object is Object) && object.is_ink_class(name_of_class)
static func are_of_same_type(object1: Variant, object2: Variant) -> bool:
if (object1 is Object) && (object2 is Object):
return object1.get_ink_class() == object2.get_ink_class()
return typeof(object1) == typeof(object2)
static func value_type_name(value_type: int) -> String:
match value_type:
ValueType.BOOL: return "Boolean"
ValueType.INT: return "Int"
ValueType.FLOAT: return "Float"
ValueType.LIST: return "List"
ValueType.STRING: return "String"
ValueType.DIVERT_TARGET: return "Divert Target"
ValueType.VARIABLE_POINTER: return "Variable Pointer"
_: return "unknown"
static func typename_of(variant) -> String:
match typeof(variant):
TYPE_NIL: return "null"
TYPE_BOOL: return "bool"
TYPE_INT: return "int"
TYPE_FLOAT: return "float"
TYPE_STRING: return "String"
TYPE_VECTOR2: return "Vector2"
TYPE_RECT2: return "Rect2"
TYPE_VECTOR3: return "Vector3"
TYPE_TRANSFORM2D: return "Transform2D"
TYPE_PLANE: return "Plane"
TYPE_QUATERNION: return "Quaternion"
TYPE_AABB: return "AABB"
TYPE_BASIS: return "Basis"
TYPE_TRANSFORM3D: return "Transform3D"
TYPE_COLOR: return "Color"
TYPE_NODE_PATH: return "NodePath"
TYPE_RID: return "RID"
TYPE_OBJECT: return variant.get_ink_class()
TYPE_DICTIONARY: return "Dictionary"
TYPE_ARRAY: return "Array"
TYPE_PACKED_BYTE_ARRAY: return "PackedByteArray"
TYPE_PACKED_INT32_ARRAY: return "PackedInt32Array"
TYPE_PACKED_FLOAT32_ARRAY: return "PackedFloat32Array"
TYPE_PACKED_STRING_ARRAY: return "PackedStringArray"
TYPE_PACKED_VECTOR2_ARRAY: return "PackedVector2Array"
TYPE_PACKED_VECTOR3_ARRAY: return "PackedVector3Array"
TYPE_PACKED_COLOR_ARRAY: return "PackedColorArray"
_: return "unknown"
# ############################################################################ #
# String Utils
# ############################################################################ #
static func trim(string_to_trim: String, characters = []) -> String:
if characters.is_empty():
return string_to_trim.strip_edges()
var length = string_to_trim.length()
var beginning = 0
var end = length
var i = 0
while i < string_to_trim.length():
var character = string_to_trim[i]
if characters.find(character) != -1:
beginning += 1
else:
break
i += 1
i = string_to_trim.length() - 1
while i >= 0:
var character = string_to_trim[i]
if characters.find(character) != -1:
end -= 1
else:
break
i -= 1
if beginning == 0 && end == length:
return string_to_trim
return string_to_trim.substr(beginning, end - beginning)
# ############################################################################ #
# Array Utils
# ############################################################################ #
static func join(joiner: String, array: Array) -> String:
var joined_string = ""
var i = 0
for element in array:
var element_string
if is_ink_class(element, "InkBase"):
element_string = element._to_string()
else:
element_string = str(element)
joined_string += element_string
if i >= 0 && i < array.size() - 1:
joined_string += joiner
i += 1
return joined_string
static func get_range(array: Array, index: int, count: int) -> Array:
if !(index >= 0 && index < array.size()):
printerr("get_range: index (%d) is out of bounds." % index)
return array.duplicate()
if index + count > array.size():
printerr("get_range: [index (%d) + count (%d)] is out of bounds." % [index, count])
return array.duplicate()
var new_array = []
var i = index
var c = 0
while (c < count):
new_array.append(array[i + c])
c += 1
return new_array
static func remove_range(array: Array, index: int, count: int) -> void:
if !(index >= 0 && index < array.size()):
printerr("get_range: index (%d) is out of bounds." % index)
return
if index + count > array.size():
printerr("get_range: [index (%d) + count (%d)] is out of bounds." % [index, count])
return
var i = index
var c = 0
while (c < count):
array.remove_at(i)
c += 1
static func array_equal(a1: Array, a2: Array, use_equals = false) -> bool:
if a1.size() != a2.size():
return false
var i = 0
while (i < a1.size()):
var first_element = a1[i]
var second_element = a2[i]
if use_equals:
if !first_element.equals(second_element):
return false
else:
i += 1
continue
else:
if first_element != second_element:
return false
else:
i += 1
continue
i += 1
return true
# ############################################################################ #
static func InkRuntime():
return Engine.get_main_loop().root.get_node("__InkRuntime")

View file

@ -0,0 +1,154 @@
# warning-ignore-all:shadowed_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkFlow
# ############################################################################ #
# Imports
# ############################################################################ #
var CallStack = load("res://addons/inkgd/runtime/callstack.gd")
# ############################################################################ #
# Self-reference
# ############################################################################ #
static func Flow():
return load("res://addons/inkgd/runtime/flow.gd")
# ############################################################################ #
var name # string
var callstack # CallStack
var output_stream # Array<InkObject>
var current_choices # Array<Choice>
func _init(static_json = null):
get_static_json(static_json)
# (String, Story) -> Flow
func _init_with_name(name, story):
self.name = name
self.callstack = CallStack.new(story, self.StaticJSON)
self.output_stream = []
self.current_choices = []
# (String, Story, Dictionary<String, Variant>) -> Flow
func _init_with_name_and_jobject(name, story, jobject):
self.name = name
self.callstack = CallStack.new(story, self.StaticJSON)
self.callstack.set_json_token(jobject["callstack"], story)
self.output_stream = self.StaticJSON.jarray_to_runtime_obj_list(jobject["outputStream"])
self.current_choices = self.StaticJSON.jarray_to_runtime_obj_list(jobject["currentChoices"])
# jchoice_threads_obj is null if 'choiceThreads' doesn't exist.
var jchoice_threads_obj = jobject.get("choiceThreads");
self.load_flow_choice_threads(jchoice_threads_obj, story)
# (SimpleJson.Writer) -> void
func write_json(writer):
writer.write_object_start()
writer.write_property("callstack", Callable(self.callstack, "write_json"))
writer.write_property(
"outputStream",
Callable(self, "_anonymous_write_property_output_stream")
)
var has_choice_threads = false
for c in self.current_choices:
c.original_thread_index = c.thread_at_generation.thread_index
if self.callstack.thread_with_index(c.original_thread_index) == null:
if !has_choice_threads:
has_choice_threads = true
writer.write_property_start("choiceThreads")
writer.write_object_start()
writer.write_property_start(c.original_thread_index)
c.thread_at_generation.write_json(writer)
writer.write_property_end()
if has_choice_threads:
writer.write_object_end()
writer.write_property_end()
writer.write_property(
"currentChoices",
Callable(self, "_anonymous_write_property_current_choices")
)
writer.write_object_end()
# (Dictionary, Story) -> void
func load_flow_choice_threads(jchoice_threads, story):
for choice in self.current_choices:
var found_active_thread = self.callstack.thread_with_index(choice.original_thread_index)
if found_active_thread != null:
choice.thread_at_generation = found_active_thread.copy()
else:
var jsaved_choice_thread = jchoice_threads[str(choice.original_thread_index)]
choice.thread_at_generation = CallStack.InkThread.new_with(jsaved_choice_thread, story)
# (SimpleJson.Writer) -> void
func _anonymous_write_property_output_stream(w):
self.StaticJSON.write_list_runtime_objs(w, self.output_stream)
# (SimpleJson.Writer) -> void
func _anonymous_write_property_current_choices(w):
w.write_array_start()
for c in self.current_choices:
self.StaticJSON.write_choice(w, c)
w.write_array_end()
func equals(ink_base) -> bool:
return false
func _to_string() -> String:
return str(self)
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type):
return type == "Flow" || super.is_ink_class(type)
func get_ink_class():
return "Flow"
static func new_with_name(name, story, static_json = null):
var flow = Flow().new(static_json)
flow._init_with_name(name, story)
return flow
static func new_with_name_and_jobject(name, story, jobject, static_json = null):
var flow = Flow().new(static_json)
flow._init_with_name_and_jobject(name, story, jobject)
return flow
# ############################################################################ #
var StaticJSON: InkStaticJSON:
get: return _static_json.get_ref()
var _static_json = WeakRef.new()
func get_static_json(static_json = null):
if static_json != null:
_static_json = weakref(static_json)
return
var InkRuntime = Engine.get_main_loop().root.get_node("__InkRuntime")
InkUtils.__assert__(InkRuntime != null,
str("[InkFlow] Could not retrieve 'InkRuntime' singleton from the scene tree."))
_static_json = weakref(InkRuntime.json)

View file

@ -0,0 +1,294 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkPath
# ############################################################################ #
const parent_id = "^"
# ############################################################################ #
class Component extends InkBase:
var index: int = 0
var name = null # String?
var is_index: bool:
get: return index >= 0
var is_parent: bool:
get: return name == parent_id
# ######################################################################## #
func _init(index_or_name):
if index_or_name is int:
var index = index_or_name
assert(index >= 0)
self.index = index
self.name = null
elif index_or_name is String:
var name = index_or_name
assert(name != null && name.length() > 0)
self.name = name
self.index = -1
# () -> Component
static func to_parent() -> Component:
return Component.new(parent_id)
# () -> String
func _to_string() -> String:
if self.is_index:
return str(index)
else:
return name
# (Component) -> bool
func equals(other_comp) -> bool:
# Simple test to make sure the object is of the right type.
if !(other_comp is Object && other_comp.is_ink_class("InkPath.Component")): return false
if other_comp.is_index == self.is_index:
if self.is_index:
return index == other_comp.index
else:
return name == other_comp.name
return false
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "InkPath.Component" || super.is_ink_class(type)
func get_ink_class():
return "InkPath.Component"
# ############################################################################ #
func get_component(index: int) -> InkPath.Component:
return self._components[index]
var is_relative: bool = false
var head: InkPath.Component:
get:
if _components.size() > 0:
return _components.front()
else:
return null
# TODO: Make inspectable
var tail: InkPath:
get:
if _components.size() >= 2:
var tail_comps = _components.duplicate()
tail_comps.pop_front()
return InkPath().new_with_components(tail_comps)
else:
return InkPath().__self()
var length: int:
get: return _components.size()
var last_component: InkPath.Component:
get:
if _components.size() > 0:
return _components.back()
else:
return null
var contains_named_component: bool:
get:
for comp in _components:
if !comp.is_index:
return true
return false
func _init():
self._components = []
func _init_with_head_tail(head, tail):
self._components = []
self._components.append(head)
self._components = self._components + self.tail._components
func _init_with_components(components, relative = false):
self._components = []
self._components = self._components + components
self.is_relative = relative
func _init_with_components_string(components_string):
self._components = []
self.components_string = components_string
# () -> InkPath
static func __self() -> InkPath:
var path = InkPath().new()
path.is_relative = true
return path
# (InkPath) -> InkPath
func path_by_appending_path(path_to_append):
var p = InkPath().new()
var upward_moves = 0
var i = 0
while(i < path_to_append._components.size()):
if path_to_append._components[i].is_parent:
upward_moves += 1
else:
break
i += 1
i = 0
while(i < self._components.size() - upward_moves):
p._components.append(self._components[i])
i += 1
i = upward_moves
while(i < path_to_append._components.size()):
p._components.append(path_to_append._components[i])
i += 1
return p
# (Component) -> InkPath
func path_by_appending_component(c):
var p = InkPath().new()
p._components = p._components + self._components
p._components.append(c)
return p
var components_string: String:
get:
if _components_string == null:
_components_string = InkUtils.join(".", _components)
if self.is_relative:
_components_string = "." + _components_string
return _components_string
set(value):
_components.clear()
_components_string = value
if (_components_string == null || _components_string.length() == 0):
return
if _components_string[0] == '.':
self.is_relative = true
_components_string = _components_string.substr(1, _components_string.length() - 1)
else:
self.is_relative = false
var components_strings = _components_string.split(".")
for _str in components_strings:
if _str.is_valid_int():
_components.append(Component.new(int(_str)))
else:
_components.append(Component.new(_str))
var _components_string # String
func _to_string() -> String:
return self.components_string
# (Component) -> bool
func equals(other_path):
# Simple test to make sure the object is of the right type.
if !(other_path is Object && other_path.is_ink_class("InkPath")): return false
if other_path._components.size() != self._components.size():
return false
if other_path.is_relative != self.is_relative:
return false
return InkUtils.array_equal(other_path._components, self._components, true)
var _components = null # Array<Component>
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
static func new_with_head_tail(head, tail):
var path = InkPath().new()
path._init_with_head_tail(head, tail)
return path
static func new_with_components(components, relative = false):
var path = InkPath().new()
path._init_with_components(components, relative)
return path
static func new_with_components_string(components_string):
var path = InkPath().new()
path._init_with_components_string(components_string)
return path
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type):
return type == "InkPath" || super.is_ink_class(type)
func get_ink_class():
return "InkPath"
static func InkPath():
return load("res://addons/inkgd/runtime/ink_path.gd")

View file

@ -0,0 +1,522 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkList
# ############################################################################ #
# (Dictionary<InkItem, int>, Array<String>, Array<InkListDefinition>)
func _init_from_csharp(items: Dictionary, origin_names: Array, origins: Array):
_dictionary = items
_origin_names = origin_names
self.origins = origins
# (InkList) -> InkList
func _init_with_ink_list(other_list: InkList):
_dictionary = other_list._dictionary.duplicate()
var other_origin_names = other_list.origin_names
if other_origin_names != null:
_origin_names = other_list.origin_names.duplicate()
if other_list.origins != null:
self.origins = other_list.origins.duplicate()
# (string, Story) -> InkList
func _init_with_origin(single_origin_list_name: String, origin_story: InkStory):
set_initial_origin_name(single_origin_list_name)
var def: InkTryGetResult = origin_story.list_definitions.try_list_get_definition(single_origin_list_name)
if def.exists:
origins = [def.result]
else:
InkUtils.throw_exception(
"InkList origin could not be found in story when constructing new list: %s" \
% single_origin_list_name
)
# (InkListItem, int) -> InkList
func _init_with_single_item(single_item: InkListItem, single_value: int):
set_item(single_item, single_value)
# (string, Story) -> InkList
static func from_string(my_list_item: String, origin_story: InkStory) -> InkList:
var list_value: InkListValue = origin_story.list_definitions.find_single_item_list_with_name(my_list_item)
if list_value:
return InkList.new_with_ink_list(list_value.value)
else:
InkUtils.throw_exception(
"Could not find the InkListItem from the string '%s' to create an InkList because " +
"it doesn't exist in the original list definition in ink." % my_list_item
)
return null
func add_item(item: InkListItem) -> void:
if item.origin_name == null:
add_item(item.item_name)
return
for origin in self.origins:
if origin.name == item.origin_name:
var int_val: InkTryGetResult = origin.try_get_value_for_item(item)
if int_val.exists:
set_item(item, int_val.result)
return
else:
InkUtils.throw_exception(
"Could not add the item '%s' to this list because it doesn't exist in the " +
"original list definition in ink." % item._to_string()
)
return
InkUtils.throw_exception(
"Failed to add item to list because the item was from a new list definition that " +
"wasn't previously known to this list. Only items from previously known lists can " +
"be used, so that the int value can be found."
)
func add_item_by_string(item_name: String) -> void:
var found_list_def: InkListDefinition = null
for origin in self.origins:
if origin.contains_item_with_name(item_name):
if found_list_def != null:
InkUtils.throw_exception(
"Could not add the item " + item_name + " to this list because it could " +
"come from either " + origin.name + " or " + found_list_def.name
)
return
else:
found_list_def = origin
if found_list_def == null:
InkUtils.throw_exception(
"Could not add the item " + item_name + " to this list because it isn't known " +
"to any list definitions previously associated with this list."
)
return
var item = InkListItem.new_with_origin_name(found_list_def.name, item_name)
var item_val: int = found_list_def.value_for_item(item)
set_item(item, item_val)
func contains_item_named(item_name: String) -> bool:
for item_key in keys():
if item_key.item_name == item_name:
return true
return false
# Array<ListDefinition>
var origins = null
var origin_of_max_item: InkListDefinition: get = get_origin_of_max_item
func get_origin_of_max_item() -> InkListDefinition:
if self.origins == null:
return null
var max_origin_name = self.max_item.key.origin_name
for origin in self.origins:
if origin.name == max_origin_name:
return origin
return null
# Array<String>
var origin_names : get = get_origin_names
func get_origin_names():
if self.size() > 0:
if _origin_names == null && self.size() > 0:
_origin_names = []
else:
_origin_names.clear()
for item_key in keys():
_origin_names.append(item_key.origin_name)
return _origin_names
var _origin_names = null # Array<String>
func set_initial_origin_name(initial_origin_name: String) -> void:
_origin_names = [ initial_origin_name ]
# (Array<String>) -> void
func set_initial_origin_names(initial_origin_names) -> void:
if initial_origin_names == null:
_origin_names = null
else:
_origin_names = initial_origin_names.duplicate()
# TODO: Make inspectable
var max_item: InkKeyValuePair: # InkKeyValuePair<InkListItem, int>
get:
var _max_item: InkKeyValuePair = InkKeyValuePair.new_with_key_value(InkListItem.null_item, 0)
for k in keys():
if (_max_item.key.is_null || get_item(k) > _max_item.value):
_max_item = InkKeyValuePair.new_with_key_value(k, get_item(k))
return _max_item
# TODO: Make inspectable
var min_item: InkKeyValuePair: # InkKeyValuePair<InkListItem, int>
get:
var _min_item: InkKeyValuePair = InkKeyValuePair.new_with_key_value(InkListItem.null_item, 0)
for k in keys():
if (_min_item.key.is_null || get_item(k) < _min_item.value):
_min_item = InkKeyValuePair.new_with_key_value(k, get_item(k))
return _min_item
# TODO: Make inspectable
var inverse: InkList: get = get_inverse
func get_inverse() -> InkList:
var list: InkList = InkList.new()
if self.origins != null:
for origin in self.origins:
for serialized_item_key in origin.items:
if !_dictionary.has(serialized_item_key):
list._dictionary[serialized_item_key] = origin.items[serialized_item_key]
return list
# TODO: Make inspectable
var all: InkList: get = get_all
func get_all() -> InkList:
var list: InkList = InkList.new()
if self.origins != null:
for origin in self.origins:
for serialized_item_key in origin.items:
list._dictionary[serialized_item_key] = origin.items[serialized_item_key]
return list
# TODO: Make inspectable
func union(other_list: InkList) -> InkList:
var union: InkList = InkList.new_with_ink_list(self)
for key in other_list._dictionary:
union._dictionary[key] = other_list._dictionary[key]
return union
# TODO: Make inspectable
func intersection(other_list: InkList) -> InkList:
var intersection: InkList = InkList.new()
for key in other_list._dictionary:
if self._dictionary.has(key):
intersection._dictionary[key] = other_list._dictionary[key]
return intersection
func has_intersection(other_list: InkList) -> bool:
for key in other_list._dictionary:
if self._dictionary.has(key):
return true
return false
# TODO: Make inspectable
func without(list_to_remove: InkList) -> InkList:
var result = InkList.new_with_ink_list(self)
for key in list_to_remove._dictionary:
result._dictionary.erase(key)
return result
func contains(other_list: InkList) -> bool:
if other_list._dictionary.is_empty() || self._dictionary.is_empty():
return false
for key in other_list._dictionary:
if !_dictionary.has(key):
return false
return true
# In the original source code 'list_item_name' is of type (String | null),
# but the method doesn't need to allow null names.
func contains_item(list_item_name: String) -> bool:
for key in self._dictionary:
var list_item = InkListItem.from_serialized_key(key)
if list_item.item_name == list_item_name:
return true
return false
func greater_than(other_list: InkList) -> bool:
if size() == 0:
return false
if other_list.size() == 0:
return true
return self.min_item.value > other_list.max_item.value
func greater_than_or_equals(other_list: InkList) -> bool:
if size() == 0:
return false
if other_list.size() == 0:
return true
return (
self.min_item.value >= other_list.min_item.value &&
self.max_item.value >= other_list.max_item.value
)
func less_than(other_list: InkList) -> bool:
if other_list.size() == 0:
return false
if size() == 0:
return true
return self.max_item.value < other_list.min_item.value
func less_than_or_equals(other_list: InkList) -> bool:
if other_list.size() == 0:
return false
if size() == 0:
return true
return (
self.max_item.value <= other_list.max_item.value &&
self.min_item.value <= other_list.min_item.value
)
func max_as_list() -> InkList:
if size() > 0:
var _max_item: InkKeyValuePair = self.max_item
return InkList.new_with_single_item(_max_item.key, _max_item.value)
else:
return InkList.new()
func min_as_list() -> InkList:
if size() > 0:
var _min_item: InkKeyValuePair = self.min_item
return InkList.new_with_single_item(_min_item.key, _min_item.value)
else:
return InkList.new()
# (Variant, Variant) -> InkList
func list_with_sub_range(min_bound, max_bound) -> InkList:
if size() == 0:
return InkList.new()
var ordered: Array = self.ordered_items
var min_value: int = 0
var max_value: int = 9_223_372_036_854_775_807 # MAX_INT
if min_bound is int:
min_value = min_bound
else:
if min_bound.is_ink_class("InkList") && min_bound.size() > 0:
min_value = min_bound.min_item.value
if max_bound is int:
max_value = max_bound
else:
if min_bound.is_ink_class("InkList") && min_bound.size() > 0:
max_value = max_bound.max_item.value
var sub_list = InkList.new()
sub_list.set_initial_origin_names(self.origin_names)
for item in ordered:
if item.value >= min_value && item.value <= max_value:
sub_list.set_item(item.key, item.value)
return sub_list
func equals(other: InkBase) -> bool:
var other_raw_list: InkList = other
# Simple test to make sure the object is of the right type.
if !(other_raw_list is Object):
return false
if !(other_raw_list.is_ink_class("InkList")):
return false
if other_raw_list.size() != self.size():
return false
for key in keys():
if (!other_raw_list.has_item(key)):
return false
return true
var ordered_items: Array: # Array<InkKeyValuePair<InkListItem, int>>
get:
var ordered: Array = []
for key in keys():
ordered.append(InkKeyValuePair.new_with_key_value(key, get_item(key)))
ordered.sort_custom(Callable(KeyValueInkListItemSorter, "sort"))
return ordered
func _to_string() -> String:
var ordered: Array = self.ordered_items
var description: String = ""
var i: int = 0
while (i < ordered.size()):
if i > 0:
description += ", "
var item = ordered[i].key
description += item.item_name
i += 1
return description
static func new_with_dictionary(other_dictionary: Dictionary) -> InkList:
var ink_list: InkList = InkList.new()
ink_list._init_with_dictionary(other_dictionary)
return ink_list
static func new_with_ink_list(other_list: InkList) -> InkList:
var ink_list: InkList = InkList.new()
ink_list._init_with_ink_list(other_list)
return ink_list
static func new_with_origin(single_origin_list_name: String, origin_story) -> InkList:
var ink_list: InkList = InkList.new()
ink_list._init_with_origin(single_origin_list_name, origin_story)
return ink_list
static func new_with_single_item(single_item: InkListItem, single_value: int) -> InkList:
var ink_list: InkList = InkList.new()
ink_list._init_with_single_item(single_item, single_value)
return ink_list
class KeyValueInkListItemSorter:
static func sort(a, b):
if a.value == b.value:
return a.key.origin_name.nocasecmp_to(b.key.origin_name) <= 0
else:
return a.value <= b.value
# ############################################################################ #
# Originally, this class would inherit Dictionary. This isn't possible in
# GDScript. Instead, this class will encapsulate a dictionary and forward
# needed calls.
# ############################################################################ #
var _dictionary: Dictionary = {}
# Name set_item instead of set to prevent shadowing 'Object.set'.
func set_item(key: InkListItem, value: int) -> void:
_dictionary[key.serialized()] = value
# Name get_item instead of get to prevent shadowing 'Object.get'.
func get_item(key: InkListItem, default = null):
return _dictionary.get(key.serialized(), default)
# Name has_item instead of has to prevent shadowing 'Object.get'.
func has_item(key: InkListItem) -> bool:
return _dictionary.has(key.serialized())
func keys() -> Array:
var deserialized_keys = []
for key in _dictionary.keys():
deserialized_keys.append(InkListItem.from_serialized_key(key))
return deserialized_keys
func size() -> int:
return _dictionary.size()
# ############################################################################ #
# Additional methods
# ############################################################################ #
func set_raw(key: String, value: int) -> void:
if OS.is_debug_build() && !(key is String):
print("Warning: Expected serialized key in InkList.set_raw().")
_dictionary[key] = value
func erase_raw(key: String) -> bool:
if OS.is_debug_build() && !(key is String):
print("Warning: Expected serialized key in InkList.erase_raw().")
return _dictionary.erase(key)
func get_raw(key: String, default = null):
if OS.is_debug_build() && !(key is String):
print("Warning: Expected serialized key in InkList.get_raw().")
return _dictionary.get(key, default)
func has_raw(key: String) -> bool:
if OS.is_debug_build() && !(key is String):
print("Warning: Expected serialized key in InkList.has_raw().")
return _dictionary.has(key)
func has_all_raw(keys: Array) -> bool:
return _dictionary.has_all(keys)
func raw_keys() -> Array:
return _dictionary.keys()
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkList" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkList"

View file

@ -0,0 +1,102 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkListDefinition
# ############################################################################ #
# Imports
# ############################################################################ #
var InkTryGetResult = preload("res://addons/inkgd/runtime/extra/try_get_result.gd")
# ############################################################################ #
var name: String: get = get_name
func get_name() -> String:
return _name
# Dictionary<InkListItem, int> => Dictionary<String, int>
# Note: 'InkListItem' should actually be serialized into a String, because it
# needs to be a value type.
var items: Dictionary: get = get_items
func get_items() -> Dictionary:
if _items == null:
_items = {}
for item_name_and_value_key in _item_name_to_values:
var item = InkListItem.new_with_origin_name(self.name, item_name_and_value_key)
_items[item.serialized()] = _item_name_to_values[item_name_and_value_key]
return _items
var _items
# ############################################################################ #
func value_for_item(item: InkListItem) -> int:
if (_item_name_to_values.has(item.item_name)):
var intVal = _item_name_to_values[item.item_name]
return intVal
else:
return 0
func contains_item(item: InkListItem) -> bool:
if item.origin_name != self.name:
return false
return _item_name_to_values.has(item.item_name)
func contains_item_with_name(item_name: String) -> bool:
return _item_name_to_values.has(item_name)
# (int) -> { result: InkListItem, exists: bool }
func try_get_item_with_value(val: int) -> InkTryGetResult:
for named_item_key in _item_name_to_values:
if (_item_name_to_values[named_item_key] == val):
return InkTryGetResult.new(
true,
InkListItem.new_with_origin_name(self.name, named_item_key)
)
return InkTryGetResult.new(false, InkListItem.null_item)
# (InkListItem) -> { result: InkListItem, exists: bool }
func try_get_value_for_item(item: InkListItem) -> InkTryGetResult:
if !item.item_name:
return InkTryGetResult.new(false, 0)
var value = _item_name_to_values.get(item.item_name)
if (!value):
InkTryGetResult.new(false, 0)
return InkTryGetResult.new(true, value)
# (String name, Dictionary<String, int>) -> InkListDefinition
func _init(name: String, items: Dictionary):
_name = name
_item_name_to_values = items
var _name: String
var _item_name_to_values: Dictionary # Dictionary<String, int>
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkListDefinition" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkListDefinition"
func _to_string() -> String:
return "[InkListDefinition \"%s\"]" % get_name()

View file

@ -0,0 +1,86 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
class_name InkListDefinitionsOrigin
# ############################################################################ #
# Imports
# ############################################################################ #
var InkTryGetResult = preload("res://addons/inkgd/runtime/extra/try_get_result.gd")
var InkListValue = load("res://addons/inkgd/runtime/values/list_value.gd")
# ############################################################################ #
# Array<InkListDefinition>
var lists: Array: get = get_lists
func get_lists() -> Array:
var list_of_lists = []
for named_list_key in _lists:
list_of_lists.append(_lists[named_list_key])
return list_of_lists
# ############################################################################ #
# (Array<InkListDefinition>) -> InkListDefinitionOrigin
func _init(lists: Array):
_lists = {} # Dictionary<String, InkListDefinition>
_all_unambiguous_list_value_cache = {} # Dictionary<String, InkListValue>()
for list in lists:
_lists[list.name] = list
for item_with_value_key in list.items:
var item = InkListItem.from_serialized_key(item_with_value_key)
var val = list.items[item_with_value_key]
var list_value = InkListValue.new_with_single_item(item, val)
_all_unambiguous_list_value_cache[item.item_name] = list_value
_all_unambiguous_list_value_cache[item.full_name] = list_value
# ############################################################################ #
# (String) -> { result: String, exists: bool }
func try_list_get_definition(name: String) -> InkTryGetResult:
if name == null:
return InkTryGetResult.new(false, null)
var definition = _lists.get(name)
if !definition:
return InkTryGetResult.new(false, null)
return InkTryGetResult.new(true, definition)
func find_single_item_list_with_name(name: String) -> InkListValue:
if _all_unambiguous_list_value_cache.has(name):
return _all_unambiguous_list_value_cache[name]
return null
# ############################################################################ #
var _lists: Dictionary # Dictionary<String, InkListDefinition>
var _all_unambiguous_list_value_cache: Dictionary # Dictionary<String, InkListValue>
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkListDefinitionsOrigin" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkListDefinitionsOrigin"

View file

@ -0,0 +1,159 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# ############################################################################ #
# !! VALUE TYPE
# ############################################################################ #
extends InkObject
class_name InkListItem
# ############################################################################ #
# Originally these were simple variables, but they are turned into properties to
# make the object "immutable". That way it can be passed around without being
# duplicated.
var origin_name:
get: return _origin_name
var _origin_name = null # String
var item_name:
get: return _item_name
var _item_name = null # String
# ############################################################################ #
# (string, string) -> InkListItem
@warning_ignore("shadowed_variable")
func _init_with_origin_name(origin_name, item_name):
self._origin_name = origin_name
self._item_name = item_name
# (string) -> InkListItem
@warning_ignore("shadowed_variable")
func _init_with_full_name(full_name):
var name_parts = full_name.split(".")
self._origin_name = name_parts[0]
self._item_name = name_parts[1]
static var null_item: InkListItem:
get: return InkListItem.new_with_origin_name(null, null)
# ############################################################################ #
var is_null: bool:
get:
return self.origin_name == null && self.item_name == null
# String
var full_name:
get:
# In C#, concatenating null produce nothing, in GDScript, it appends "Null".
return (
(self.origin_name if self.origin_name else "?") + "." +
(self.item_name if self.item_name else "")
)
# ############################################################################ #
# () -> String
func _to_string() -> String:
return self.full_name
# (InkObject) -> bool
func equals(obj: InkBase) -> bool:
if obj.is_ink_class("InkListItem"):
var other_item = obj
return (
other_item.item_name == self.item_name &&
self.other_item.origin_name == self.origin_name
)
return false
# ############################################################################ #
# (string, string) -> InkListItem
@warning_ignore("shadowed_variable")
static func new_with_origin_name(origin_name, item_name) -> InkListItem:
var list_item = InkListItem.new()
list_item._init_with_origin_name(origin_name, item_name)
return list_item
# (string) -> InkListItem
@warning_ignore("shadowed_variable")
static func new_with_full_name(full_name) -> InkListItem:
var list_item = InkListItem.new()
list_item._init_with_full_name(full_name)
return list_item
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkListItem" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkListItem"
# ############################################################################ #
# These methods did not exist in the original C# code. Their purpose is to
# make `InkListItem` mimic the value-type semantics of the original
# struct, as well as offering a serialization mechanism to use `InkListItem`
# as keys in dictionaries.
# Returns a `SerializedInkListItem` representing the current
# instance. The result is intended to be used as a key inside a Map.
func serialized() -> String:
# We are simply using a JSON representation as a value-typed key.
var json_print = JSON.stringify(
{ "originName": self.origin_name, "itemName": self.item_name }
)
return json_print
# Reconstructs a `InkListItem` from the given SerializedInkListItem.
#
# (String) -> InkListItem
static func from_serialized_key(key: String) -> InkListItem:
var obj = JSON.parse_string(key)
if !InkListItem._is_like_ink_list_item(obj):
return InkListItem.null_item
return InkListItem.new_with_origin_name(obj["originName"], obj["itemName"])
# Determines whether the given item is sufficiently `InkListItem`-like
# to be used as a template when reconstructing the InkListItem.
#
# (Variant) -> bool
static func _is_like_ink_list_item(item) -> bool:
if !(item is Dictionary):
return false
if !(item.has("originName") && item.has("itemName")):
return false
if !(item["originName"] is String):
return false
if !(item["itemName"] is String):
return false
return true

View file

@ -0,0 +1,57 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkProfiler
func _init():
pass
# () -> String
func report() -> String:
return ""
# () -> void
func pre_continue() -> void:
pass
# () -> void
func post_continue() -> void:
pass
# () -> void
func pre_step() -> void:
pass
# (CallStack) -> void
func step(callstack: InkCallStack) -> void:
pass
# () -> void
func post_step() -> void:
pass
func step_length_record() -> String:
return ""
func mega_log() -> String:
return ""
func pre_snapshot() -> void:
pass
func post_snapshot() -> void:
pass
func millisecs(watch: InkStopWatch) -> float:
return 0.0
static func format_millisecs(num: float) -> String:
return ""

View file

@ -0,0 +1,41 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# ############################################################################ #
# !! VALUE TYPE
# ############################################################################ #
# Search results are never duplicated / passed around so they don't need to
# be either immutable or have a 'duplicate' method.
extends InkBase
class_name InkSearchResult
# ############################################################################ #
var obj: InkObject = null
var approximate: bool = false
var correct_obj: InkObject:
get: return null if approximate else obj
var container: InkContainer:
get: return InkUtils.as_or_null(obj, "InkContainer")
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "SearchResult" || super.is_ink_class(type)
func get_ink_class() -> String:
return "SearchResult"

View file

@ -0,0 +1,588 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkSimpleJSON
# ############################################################################ #
# (String) -> Dictionary<String, Variant>
static func text_to_dictionary(text: String) -> Dictionary:
return Reader.new(text).to_dictionary()
# (String) -> Array<Variant>
static func text_to_array(text: String) -> Array:
return Reader.new(text).to_array()
class Reader extends InkBase:
# (String) -> Reader
func _init(text: String):
_text = text
_offset = 0
skip_whitespace()
_root_object = read_object()
# () -> Dictionary<String, Variant>
func to_dictionary() -> Dictionary:
return _root_object
# () -> Array<Variant>
func to_array() -> Array:
return _root_object
# (String) -> bool
func is_number_char(c: String) -> bool:
if c.length() > 1:
return false
return c.is_valid_int() || c == "." || c == "-" || c == "+" || c == 'E' || c == 'e'
# (String) -> bool
func is_first_number_char(c: String) -> bool:
if c.length() > 1:
return false
return c.is_valid_int() || c == "-" || c == "+"
# () -> Variant
func read_object():
var current_char = _text[_offset]
if current_char == "{":
return read_dictionary()
elif current_char == "[":
return read_array()
elif current_char == "\"":
return read_string()
elif is_first_number_char(current_char):
return read_number()
elif try_read("true"):
return true
elif try_read("false"):
return false
elif try_read("null"):
return null
InkUtils.throw_exception("Unhandled object type in JSON: %s" % _text.substr(_offset, 30))
return JsonError.new()
# () -> Dictionary<String, Variant>?
func read_dictionary():
var dict = {} # Dictionary<String, Variant>
if !expect("{"):
return null
skip_whitespace()
if try_read("}"):
return dict
var first_time = true
while first_time || try_read(","):
first_time = false
skip_whitespace()
var key = read_string()
if !expect(key != null, "dictionary key"):
return null
skip_whitespace()
if !expect(":"):
return null
skip_whitespace()
var val = read_object()
if !expect(val != null, "dictionary value"):
return null
dict[key] = val
skip_whitespace()
if !expect("}"):
return null
return dict
# () -> Array<Variant>?
func read_array():
var list = []
if !expect("["):
return null
skip_whitespace()
if try_read("]"):
return list
var first_time = true
while first_time || try_read(","):
first_time = false
skip_whitespace()
var val = read_object()
list.append(val)
skip_whitespace()
if !expect("]"):
return null
return list
# () -> String?
func read_string():
if !expect("\""):
return null
var sb = ""
while(_offset < _text.length()):
var c = _text[_offset]
if c == "\\":
_offset += 1
if _offset >= _text.length():
InkUtils.throw_exception("Unexpected EOF while reading string")
return null
c = _text[_offset]
match c:
"\"", "\\", "/":
sb += c
"n":
sb += "\n"
"t":
sb += "\t"
"r", "b", "f":
pass
"u":
if _offset + 4 >= _text.length():
InkUtils.throw_exception("Unexpected EOF while reading string")
return null
var digits = _text.substr(_offset + 1, 4)
var test_json_conv = JSON.new()
test_json_conv.parse("\"\\u" + digits + "\"")
var json_parse_result = test_json_conv.get_data()
if json_parse_result.error != OK:
InkUtils.throw_exception("Invalid Unicode escape character at offset %d" % (_offset - 1))
return null
sb += json_parse_result.result
_offset += 4
break
_:
InkUtils.throw_exception("Invalid Unicode escape character at offset %d " % (_offset - 1))
return null
elif c == "\"":
break
else:
sb += c
_offset += 1
if !expect("\""):
return null
return sb
# () -> Variant
func read_number():
var start_offset = _offset
var is_float = false
while(_offset < _text.length()):
var c = _text[_offset]
if (c == "." || c == "e" || c == "E"): is_float = true
if is_number_char(c):
_offset += 1
continue
else:
break
_offset += 1
var num_str = _text.substr(start_offset, _offset - start_offset)
if is_float:
if num_str.is_valid_float():
return float(num_str)
else:
if num_str.is_valid_int():
return int(num_str)
InkUtils.throw_exception("Failed to parse number value: " + num_str)
return JsonError.new()
# (String) -> bool
func try_read(text_to_read: String) -> bool:
if _offset + text_to_read.length() > _text.length():
return false
var i = 0
while (i < text_to_read.length()):
if text_to_read[i] != _text[_offset + i]:
return false
i += 1
_offset += text_to_read.length()
return true
# (bool | String, String) -> bool
func expect(condition_or_expected_str, message = null) -> bool:
var _condition = false
if condition_or_expected_str is String:
_condition = try_read(condition_or_expected_str)
elif condition_or_expected_str is bool:
_condition = condition_or_expected_str
if !_condition:
if message == null:
message = "Unexpected token"
else:
message = "Expected " + message
message += str(" at offset ", _offset)
InkUtils.throw_exception(message)
return false
return true
func skip_whitespace():
while _offset < _text.length():
var c = _text[_offset]
if c == " " || c == "\t" || c == "\n" || c == "\r":
_offset += 1
else:
break
var _text = null # String
var _offset: int = 0 # int
var _root_object # Variant
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type: String) -> bool:
return type == "InkSimpleJSON.Reader" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkSimpleJSON.Reader"
class Writer extends InkBase:
# ######################################################################## #
# Imports
# ######################################################################## #
var InkStringWriter := load("res://addons/inkgd/runtime/extra/string_writer.gd") as GDScript
var InkStateElement := load("res://addons/inkgd/runtime/extra/state_element.gd") as GDScript
# (String) -> Writer
func _init():
self._writer = InkStringWriter.new()
# (Callable) -> void
func write_object(inner: Callable) -> void:
write_object_start()
inner.call(self)
write_object_end()
func write_object_start() -> void:
start_new_object(true)
self._state_stack.push_front(InkStateElement.new(InkStateElement.State.OBJECT))
self._writer.write("{")
func write_object_end() -> void:
assert_that(self.state == InkStateElement.State.OBJECT)
self._writer.write("}")
self._state_stack.pop_front()
# These two methods don't need to be implemented in GDScript.
#
# public void WriteProperty(string name, Action<Writer> inner)
# public void WriteProperty(int id, Action<Writer> inner)
# Also include:
# void WriteProperty<T>(T name, Action<Writer> inner)
# (String, Variant) -> void
func write_property(name: String, content) -> void:
if (content is String || content is int || content is bool):
write_property_start(name)
write(content)
write_property_end()
elif content is Callable:
write_property_start(name)
content.call(self)
write_property_end()
else:
push_error("Wrong type for 'content': %s" % str(content))
# These two methods don't need to be implemented in GDScript.
#
# public void WritePropertyStart(string name)
# public void WritePropertyStart(int id)
# () -> void
func write_property_end() -> void:
assert_that(self.state == InkStateElement.State.PROPERTY)
assert_that(self.child_count == 1)
self._state_stack.pop_front()
# (String) -> void
func write_property_name_start() -> void:
assert_that(self.state == InkStateElement.State.OBJECT)
if self.child_count > 0:
self._writer.write(',')
self._writer.write('"')
increment_child_count()
self._state_stack.push_front(InkStateElement.new(InkStateElement.State.PROPERTY))
self._state_stack.push_front(InkStateElement.new(InkStateElement.State.PROPERTY_NAME))
# () -> void
func write_property_name_end() -> void:
assert_that(self.state == InkStateElement.State.PROPERTY_NAME)
self._writer.write('":')
self._state_stack.pop_front()
# (String) -> void
func write_property_name_inner(string: String) -> void:
assert_that(self.state == InkStateElement.State.PROPERTY_NAME)
self._writer.write(string)
# (Variant) -> void
func write_property_start(name) -> void:
assert_that(self.state == InkStateElement.State.OBJECT)
if self.child_count > 0:
self._writer.write(',')
self._writer.write('"')
self._writer.write(str(name))
self._writer.write('":')
increment_child_count()
_state_stack.push_front(InkStateElement.new(InkStateElement.State.PROPERTY))
# () -> void
func write_array_start() -> void:
start_new_object(true)
_state_stack.push_front(InkStateElement.new(InkStateElement.State.ARRAY))
_writer.write("[")
# () -> void
func write_array_end() -> void:
assert_that(self.state == InkStateElement.State.ARRAY)
_writer.write("]")
_state_stack.pop_front()
# This method didn't exist as-is in the original implementation.
# (Variant) -> void
func write(content) -> void:
if content is int:
write_int(content)
elif content is float:
write_float(content)
elif content is String:
write_string(content)
elif content is bool:
write_bool(content)
else:
push_error("Wrong type for 'content': %s" % str(content))
# (int) -> void
func write_int(i: int) -> void:
start_new_object(false)
_writer.write(str(i))
# (float) -> void
func write_float(f: float) -> void:
start_new_object(false)
var float_str = str(f)
# We could probably use 3.402823e+38, but keeping
# ±3.4e+38 for compatibility with the reference implementation.
if float_str == "inf":
_writer.write("3.4e+38")
elif float_str == "-inf":
_writer.write("-3.4e+38")
elif float_str == "nan":
_writer.write("0.0")
else:
_writer.write(float_str)
# The exponent part is defensive as Godot doesn't seem to convert
# floats to string in such a way.
if !("." in float_str) && !("e" in float_str) && !("E" in float_str):
_writer.write(".0")
# (String, bool) -> void
func write_string(string: String, escape: bool = true):
start_new_object(false)
_writer.write('"')
if escape:
write_escaped_string(string)
else:
_writer.write(string)
_writer.write('"')
# (bool) -> void
func write_bool(b: bool) -> void:
start_new_object(false)
_writer.write("true" if b else "false")
# () -> void
func write_null() -> void:
start_new_object(false)
_writer.write("null")
# () -> void
func write_string_start() -> void:
start_new_object(true)
_state_stack.push_front(InkStateElement.new(InkStateElement.State.STRING))
_writer.write('"')
# () -> void
func write_string_end() -> void:
assert_that(state == InkStateElement.State.STRING)
_writer.write('"')
_state_stack.pop_front()
# (string, bool) -> void
func write_string_inner(string: String, escape: bool = true) -> void:
assert_that(self.state == InkStateElement.State.STRING)
if escape:
write_escaped_string(string)
else:
_writer.write(string)
# (String) -> void
func write_escaped_string(string: String) -> void:
for c in string:
if c < ' ':
match c:
"\n":
_writer.write("\\n")
"\t":
_writer.write("\\t")
else:
match c:
'\\', '"':
_writer.write("\\")
_writer.write(c)
_:
_writer.write(c)
# (bool) -> void
func start_new_object(container: bool) -> void:
if container:
assert_that(
self.state == InkStateElement.State.NONE ||
self.state == InkStateElement.State.PROPERTY ||
self.state == InkStateElement.State.ARRAY
)
else:
assert_that(
self.state == InkStateElement.State.PROPERTY ||
self.state == InkStateElement.State.ARRAY
)
if self.state == InkStateElement.State.ARRAY && self.child_count > 0:
_writer.write(",")
if self.state == InkStateElement.State.PROPERTY:
assert_that(self.child_count == 0)
if (
self.state == InkStateElement.State.ARRAY ||
self.state == InkStateElement.State.PROPERTY
):
increment_child_count()
var state: int: # StateElement.State
get:
if _state_stack.size() > 0:
return _state_stack.front().type
else:
return InkStateElement.State.NONE
var child_count: int: # int
get:
if _state_stack.size() > 0:
return _state_stack.front().child_count
else:
return 0
# () -> void
func increment_child_count() -> void:
assert_that(_state_stack.size() > 0)
var curr_el = _state_stack.pop_front()
curr_el.child_count += 1
_state_stack.push_front(curr_el)
# (bool) -> void
func assert_that(condition: bool) -> void:
if OS.is_debug_build():
return
if !condition:
push_error("Assert failed while writing JSON")
assert(condition)
# () -> String
func _to_string() -> String:
return _writer._to_string()
var _state_stack: Array = [] # Array<StateElement>
var _writer: InkStringWriter
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type: String) -> bool:
return type == "InkSimpleJSON.Writer" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkSimpleJSON.Writer"
class JsonError:
func init():
pass

View file

@ -0,0 +1,105 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkStatePatch
# ############################################################################ #
# Imports
# ############################################################################ #
var InkTryGetResult := preload("res://addons/inkgd/runtime/extra/try_get_result.gd") as GDScript
var InkStringSet := preload("res://addons/inkgd/runtime/extra/string_set.gd") as GDScript
# ############################################################################ #
# Dictionary<String, InkObject>
var globals: Dictionary: get = get_globals
func get_globals() -> Dictionary:
return _globals
# StringSet
var changed_variables: InkStringSet: get = get_changed_variables
func get_changed_variables() -> InkStringSet:
return _changed_variables
# Dictionary<InkContainer, int>
var visit_counts: Dictionary: get = get_visit_counts
func get_visit_counts() -> Dictionary:
return _visit_counts
# Dictionary<InkContainer, int>
var turn_indices : get = get_turn_indices
func get_turn_indices() -> Dictionary:
return _turn_indices
# ############################################################################ #
func _init(to_copy: InkStatePatch):
if to_copy != null:
_globals = to_copy._globals.duplicate()
_changed_variables = to_copy._changed_variables.duplicate()
_visit_counts = to_copy._visit_counts.duplicate()
_turn_indices = to_copy._turn_indices.duplicate()
else:
_globals = {}
_changed_variables = InkStringSet.new()
_visit_counts = {}
_turn_indices = {}
# (String) -> { exists: bool, result: InkObject }
func try_get_global(name) -> InkTryGetResult:
if _globals.has(name):
return InkTryGetResult.new(true, _globals[name])
return InkTryGetResult.new(false, null)
func set_global(name: String, value: InkObject) -> void:
_globals[name] = value
func add_changed_variable(name: String) -> void:
_changed_variables.append(name)
# (InkContainer) -> { exists: bool, result: int }
func try_get_visit_count(container) -> InkTryGetResult:
if _visit_counts.has(container):
return InkTryGetResult.new(true, _visit_counts[container])
return InkTryGetResult.new(false, 0)
func set_visit_count(container: InkContainer, index: int) -> void:
_visit_counts[container] = index
func set_turn_index(container: InkContainer, index: int) -> void:
_turn_indices[container] = index
# (InkContainer) -> { exists: bool, result: int }
func try_get_turn_index(container) -> InkTryGetResult:
if _turn_indices.has(container):
return InkTryGetResult.new(true, _turn_indices[container])
return InkTryGetResult.new(false, 0)
var _globals: Dictionary
var _changed_variables: InkStringSet
var _visit_counts: Dictionary
var _turn_indices: Dictionary
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "StatePatch" || super.is_ink_class(type)
func get_ink_class() -> String:
return "StatePatch"

View file

@ -0,0 +1,200 @@
# warning-ignore-all:unused_class_variable
# warning-ignore-all:shadowed_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends Node
# Hiding this type to prevent registration of "private" nodes.
# See https://github.com/godotengine/godot-proposals/issues/1047
# class_name InkRuntimeNode
# Expected to be added to the SceneTree as a singleton object.
# ############################################################################ #
# Imports
# ############################################################################ #
var InkStaticJSON := load("res://addons/inkgd/runtime/static/json.gd") as GDScript
var InkStaticNativeFunctionCall := load("res://addons/inkgd/runtime/static/native_function_call.gd") as GDScript
# ############################################################################ #
# Signals
# ############################################################################ #
## Emitted when the runtime encountered an exception. Exception are not
## recoverable and may corrupt the state. They are the consequence of either
## a programmer error or a bug in the runtime.
signal exception_raised(message, stack_trace)
# ############################################################################ #
# Properties
# ############################################################################ #
# Skips saving global values that remain equal to the initial values that were
# declared in Ink.
var dont_save_default_values: bool = true
## Uses `assert` instead of `push_error` to report critical errors, thus
## making them more explicit during development.
var stop_execution_on_exception: bool = true
## Uses `assert` instead of `push_error` to report story errors, thus
## making them more explicit during development.
var stop_execution_on_error: bool = true
# ############################################################################ #
var should_pause_execution_on_runtime_error: bool: get = get_speore, set = set_speore
func get_speore() -> bool:
printerr(
"'should_pause_execution_on_runtime_error' is deprecated, " +
"use 'stop_execution_on_exception' instead."
)
return stop_execution_on_exception
func set_speore(value: bool):
printerr(
"'should_pause_execution_on_runtime_error' is deprecated, " +
"use 'stop_execution_on_exception' instead."
)
stop_execution_on_exception = value
var should_pause_execution_on_story_error: bool: get = get_speose, set = set_speose
func get_speose() -> bool:
printerr(
"'should_pause_execution_on_story_error' is deprecated, " +
"use 'stop_execution_on_error' instead."
)
return stop_execution_on_error
func set_speose(value: bool):
printerr(
"'should_pause_execution_on_story_error' is deprecated, " +
"use 'stop_execution_on_error' instead."
)
stop_execution_on_error = value
# ############################################################################ #
# "Static" Properties
# ############################################################################ #
var native_function_call: InkStaticNativeFunctionCall = InkStaticNativeFunctionCall.new()
var json: InkStaticJSON = InkStaticJSON.new(native_function_call)
# ############################################################################ #
# Internal Properties
# ############################################################################ #
# Recorded exceptions don't emit the 'exception' signal, since they are
# expected to be processed by the story and emitted through 'on_error'.
var record_story_exceptions: bool = false
var current_story_exceptions: Array = []
var _argument_exception_raised: bool
var _exception_raised: bool
# ############################################################################ #
# Overrides
# ############################################################################ #
func _init():
name = "__InkRuntime"
# ############################################################################ #
# Internal Methods
# ############################################################################ #
func clear_raised_exceptions() -> bool:
if _argument_exception_raised:
_argument_exception_raised = false
return true
if _argument_exception_raised:
_argument_exception_raised = false
return true
return false
func handle_exception(message: String) -> void:
var exception_message = "EXCEPTION: %s" % message
var stack_trace = _get_stack_trace()
_handle_generic_exception(
exception_message,
stop_execution_on_exception,
stack_trace
)
_exception_raised
emit_signal("exception_raised", exception_message, stack_trace)
func handle_argument_exception(message: String) -> void:
var exception_message = "ARGUMENT EXCEPTION: %s" % message
var stack_trace = _get_stack_trace()
_handle_generic_exception(
exception_message,
stop_execution_on_error,
stack_trace
)
_argument_exception_raised = true
emit_signal("exception_raised", exception_message, stack_trace)
func handle_story_exception(message: String, use_end_line_number: bool, metadata) -> void:
# When exceptions are "recorded", they are not reported immediately.
# 'Story' will take care of that at the end of the step.
if record_story_exceptions:
current_story_exceptions.append(StoryError.new(message, use_end_line_number, metadata))
else:
var exception_message = "STORY EXCEPTION: %s" % message
var stack_trace = _get_stack_trace()
_handle_generic_exception(exception_message, stop_execution_on_error, stack_trace)
emit_signal("exception_raised", exception_message, stack_trace)
# ############################################################################ #
# Private Methods
# ############################################################################ #
func _handle_generic_exception(
message: String,
should_pause_execution: bool,
stack_trace: PackedStringArray
) -> void:
if OS.is_debug_build():
if should_pause_execution:
assert(false, message)
elif Engine.is_editor_hint():
printerr(message)
if stack_trace.size() > 0:
printerr("Stack trace:")
for line in stack_trace:
printerr(line)
else:
push_error(message)
func _get_stack_trace() -> PackedStringArray:
var trace := PackedStringArray()
var i = 1
for stack_element in get_stack():
if i <= 3:
i += 1
continue
trace.append(str(
" ", (i - 3), " - ", stack_element["source"], ":",
stack_element["line"], " - at function: ", stack_element["function"]
))
i += 1
return trace

View file

@ -0,0 +1,623 @@
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkStaticJSON
# In the C# code this class has only static methods. In the GDScript, it will rather
# be a unique object, added to the InkRuntime singleton.
# ############################################################################ #
var InkValue = load("res://addons/inkgd/runtime/values/value.gd")
var InkStringValue = load("res://addons/inkgd/runtime/values/string_value.gd")
var InkDivertTargetValue = load("res://addons/inkgd/runtime/values/divert_target_value.gd")
var InkVariablePointerValue = load("res://addons/inkgd/runtime/values/variable_pointer_value.gd")
var InkListValue = load("res://addons/inkgd/runtime/values/list_value.gd")
var InkList = load("res://addons/inkgd/runtime/lists/ink_list.gd")
var InkListDefinition = load("res://addons/inkgd/runtime/lists/list_definition.gd")
var InkListDefinitionsOrigin = load("res://addons/inkgd/runtime/lists/list_definitions_origin.gd")
# ############################################################################ #
# (Array<Variant>, bool) -> Array
func jarray_to_runtime_obj_list(jarray: Array, skip_last = false) -> Array:
var count = jarray.size()
if skip_last:
count -= 1
var list = []
var i = 0
while (i < count):
var jtok = jarray[i]
var runtime_obj = jtoken_to_runtime_object(jtok)
list.append(runtime_obj)
i += 1
return list
# (self.Json.Writer, Dictionary<String, InkObject>) -> void
func write_dictionary_runtime_objs(writer, dictionary: Dictionary) -> void:
writer.write_object_start()
for key in dictionary:
writer.write_property_start(key)
write_runtime_object(writer, dictionary[key])
writer.write_property_end()
writer.write_object_end()
# (self.Json.Writer, Array<InkObject>) -> void
func write_list_runtime_objs(writer, list: Array) -> void:
writer.write_array_start()
for val in list:
write_runtime_object(writer, val)
writer.write_array_end()
# (self.Json.Writer, Array<Int>) -> void
func write_int_dictionary(writer, dict: Dictionary) -> void:
writer.write_object_start()
for key in dict:
writer.write_property(key, dict[key])
writer.write_object_end()
# (self.Json.Writer, InkObject) -> void
func write_runtime_object(writer, obj: InkObject) -> void:
var container = InkUtils.as_or_null(obj, "InkContainer")
if container:
write_runtime_container(writer, container)
return
var divert = InkUtils.as_or_null(obj, "Divert")
if divert:
var div_type_key = "->" # String
if divert.is_external:
div_type_key = "x()"
elif divert.pushes_to_stack:
if divert.stack_push_type == Ink.PushPopType.FUNCTION:
div_type_key = "f()"
elif divert.stackPushType == Ink.PushPopType.TUNNEL:
div_type_key = "->t->"
var target_str = null # String
if divert.has_variable_target:
target_str = divert.variable_divert_name
else:
target_str = divert.target_path_string
writer.write_object_start()
writer.write_property(div_type_key, target_str)
if divert.has_variable_target:
writer.write_property("var", true)
if divert.is_conditional:
writer.write_property("c", true)
if divert.external_args > 0:
writer.write_property("exArgs", divert.external_args)
writer.write_object_end()
return
var choice_point = InkUtils.as_or_null(obj, "ChoicePoint")
if choice_point:
writer.write_object_start()
writer.write_property("*", choice_point.path_string_on_choice)
writer.write_property("flg", choice_point.flags)
writer.write_object_end()
return
var bool_val = InkUtils.as_or_null(obj, "BoolValue")
if bool_val:
writer.write(bool_val.value)
return
var int_val = InkUtils.as_or_null(obj, "IntValue")
if int_val:
writer.write(int_val.value)
return
var float_val = InkUtils.as_or_null(obj, "FloatValue")
if float_val:
writer.write(float_val.value)
return
var str_val = InkUtils.as_or_null(obj, "StringValue")
if str_val:
if str_val.is_newline:
writer.write_string("\\n", false)
else:
writer.write_string_start()
writer.write_string_inner("^")
writer.write_string_inner(str_val.value)
writer.write_string_end()
return
var list_val = InkUtils.as_or_null(obj, "ListValue")
if list_val:
write_ink_list(writer, list_val)
return
var div_target_val = InkUtils.as_or_null(obj, "DivertTargetValue")
if div_target_val:
writer.write_object_start()
writer.write_property("^->", div_target_val.value.components_string)
writer.write_object_end()
return
var var_ptr_val = InkUtils.as_or_null(obj, "VariablePointerValue")
if var_ptr_val:
writer.write_object_start()
writer.write_property("^var", var_ptr_val.value)
writer.write_property("ci", var_ptr_val.context_index)
writer.write_object_end()
return
var glue = InkUtils.as_or_null(obj, "Glue")
if glue:
writer.write("<>")
return
var control_cmd = InkUtils.as_or_null(obj, "ControlCommand")
if control_cmd:
writer.write(self._control_command_names[control_cmd.command_type])
return
var native_func = InkUtils.as_or_null(obj, "NativeFunctionCall")
if native_func:
var name = native_func.name
if name == "^": name = "L^"
writer.write(name)
return
var var_ref = InkUtils.as_or_null(obj, "VariableReference")
if var_ref:
writer.write_object_start()
var read_count_path = var_ref.path_string_for_count
if read_count_path != null:
writer.write_property(["CNT?"], read_count_path)
else:
writer.write_property(["VAR?"], var_ref.name)
writer.write_object_end()
return
var var_ass = InkUtils.as_or_null(obj, "VariableAssignment")
if var_ass:
writer.write_object_start()
var key = "VAR=" if var_ass.is_global else "temp="
writer.write_property(key, var_ass.variable_name)
if !var_ass.is_new_declaration:
writer.write_property("re", true)
writer.write_object_end()
return
var void_obj = InkUtils.as_or_null(obj, "Void")
if void_obj:
writer.write("void")
return
# Legacy Tags (replaced in 1.1+)
var tag = InkUtils.as_or_null(obj, "Tag")
if tag:
writer.write_object_start()
writer.write_property("#", tag.text)
writer.write_object_end()
return
var choice = InkUtils.as_or_null(obj, "Choice")
if choice:
write_choice(writer, choice)
return
InkUtils.throw_exception("Failed to convert runtime object to Json token: %s" % obj)
return
# (Dictionary<String, Variant>) -> Dictionary<String, InkObject>
func jobject_to_dictionary_runtime_objs(jobject: Dictionary) -> Dictionary:
var dict = {}
for key in jobject:
dict[key] = jtoken_to_runtime_object(jobject[key])
return dict
# (Dictionary<String, Variant>) -> Dictionary<String, int>
func jobject_to_int_dictionary(jobject: Dictionary) -> Dictionary:
var dict = {}
for key in jobject:
dict[key] = int(jobject[key])
return dict
# (Variant) -> InkObject
func jtoken_to_runtime_object(token) -> InkObject:
if token is int || token is float || token is bool:
return InkValue.create(token)
if token is String:
var _str = token
var first_char = _str[0]
if first_char == "^":
return InkStringValue.new_with(_str.substr(1, _str.length() - 1))
elif first_char == "\n" && _str.length() == 1:
return InkStringValue.new_with("\n")
if _str == "<>": return InkGlue.new()
var i = 0
while (i < _control_command_names.size()):
var cmd_name = _control_command_names[i]
if _str == cmd_name:
return InkControlCommand.new(i)
i += 1
if _str == "L^": _str = "^"
if _static_native_function_call.call_exists_with_name(_str):
return InkNativeFunctionCall.call_with_name(_str, _static_native_function_call)
if _str == "->->":
return InkControlCommand.pop_tunnel()
elif _str == "~ret":
return InkControlCommand.pop_function()
if _str == "void":
return InkVoid.new()
if token is Dictionary:
var obj = token
var prop_value
if obj.has("^->"):
prop_value = obj["^->"]
return InkDivertTargetValue.new_with(
InkPath.new_with_components_string(str(prop_value))
)
if obj.has("^var"):
prop_value = obj["^var"]
var var_ptr = InkVariablePointerValue.new_with_context(str(prop_value))
if (obj.has("ci")):
prop_value = obj["ci"]
var_ptr.context_index = int(prop_value)
return var_ptr
var is_divert = false
var pushes_to_stack = false
var div_push_type = Ink.PushPopType.FUNCTION
var external = false
if obj.has("->"):
prop_value = obj["->"]
is_divert = true
elif obj.has("f()"):
prop_value = obj["f()"]
is_divert = true
pushes_to_stack = true
div_push_type = Ink.PushPopType.FUNCTION
elif obj.has("->t->"):
prop_value = obj["->t->"]
is_divert = true
pushes_to_stack = true
div_push_type = Ink.PushPopType.TUNNEL
elif obj.has("x()"):
prop_value = obj["x()"]
is_divert = true
external = true
pushes_to_stack = false
div_push_type = Ink.PushPopType.FUNCTION
if is_divert:
var divert = InkDivert.new()
divert.pushes_to_stack = pushes_to_stack
divert.stack_push_type = div_push_type
divert.is_external = external
var target = str(prop_value)
if obj.has("var"):
prop_value = obj["var"]
divert.variable_divert_name = target
else:
divert.target_path_string = target
divert.is_conditional = obj.has("c")
#if divert.is_conditional: prop_value = obj["c"]
if external:
if obj.has("exArgs"):
prop_value = obj["exArgs"]
divert.external_args = int(prop_value)
return divert
if obj.has("*"):
prop_value = obj["*"]
var choice = InkChoicePoint.new()
choice.path_string_on_choice = str(prop_value)
if obj.has("flg"):
prop_value = obj["flg"]
choice.flags = int(prop_value)
return choice
if obj.has("VAR?"):
prop_value = obj["VAR?"]
return InkVariableReference.new(str(prop_value))
elif obj.has("CNT?"):
prop_value = obj["CNT?"]
var read_count_var_ref = InkVariableReference.new()
read_count_var_ref.path_string_for_count = str(prop_value)
return read_count_var_ref
var is_var_ass = false
var is_global_var = false
if obj.has("VAR="):
prop_value = obj["VAR="]
is_var_ass = true
is_global_var = true
elif obj.has("temp="):
prop_value = obj["temp="]
is_var_ass = true
is_global_var = false
if is_var_ass:
var var_name = str(prop_value)
var is_new_decl = !obj.has("re")
var var_ass = InkVariableAssignment.new_with(var_name, is_new_decl)
var_ass.is_global = is_global_var
return var_ass
# Legacy Tags with texts (replaced in 1.1+)
if obj.has("#"):
prop_value = obj["#"]
return InkTag.new(str(prop_value))
if obj.has("list"):
prop_value = obj["list"]
var list_content = prop_value
var raw_list = InkList.new()
if obj.has("origins"):
prop_value = obj["origins"]
var names_as_objs = prop_value
raw_list.set_initial_origin_names(names_as_objs)
for name_to_val_key in list_content:
var item = InkListItem.new_with_full_name(name_to_val_key)
var val = list_content[name_to_val_key]
raw_list.set_item(item, val)
return InkListValue.new_with(raw_list)
if obj.has("originalChoicePath"):
return jobject_to_choice(obj)
if token is Array:
var container = jarray_to_container(token)
return container
if token == null:
return null
InkUtils.throw_exception("Failed to convert token to runtime object: %s" % str(token))
return null
# (self.Json.Writer, InkContainer, Bool) -> void
func write_runtime_container(writer, container: InkContainer, without_name = false) -> void:
writer.write_array_start()
for c in container.content:
write_runtime_object(writer, c)
var named_only_content = container.named_only_content
var count_flags = container.count_flags
var has_name_property = (container.name != null) && !without_name
var has_terminator = named_only_content != null || count_flags > 0 || has_name_property
if has_terminator:
writer.write_object_start()
if named_only_content != null:
for named_content_key in named_only_content:
var name = named_content_key
var named_container = InkUtils.as_or_null(named_only_content[named_content_key], "InkContainer")
writer.write_property_start(name)
write_runtime_container(writer, named_container, true)
writer.write_property_end()
if count_flags > 0:
writer.write_property("#f", count_flags)
if has_name_property:
writer.write_property("#n", container.name)
if has_terminator:
writer.write_object_end()
else:
writer.write_null()
writer.write_array_end()
# (Array<Variant>) -> InkContainer
func jarray_to_container(jarray: Array) -> InkContainer:
var container = InkContainer.new()
container.content = jarray_to_runtime_obj_list(jarray, true)
var terminating_obj = InkUtils.as_or_null(jarray.back(), "Dictionary") # Dictionary<string, Variant>
if terminating_obj != null:
var named_only_content = {} # new Dictionary<String, InkObject>
for key in terminating_obj:
if key == "#f":
container.count_flags = int(terminating_obj[key])
elif key == "#n":
container.name = str(terminating_obj[key])
else:
var named_content_item = jtoken_to_runtime_object(terminating_obj[key])
var named_sub_container = InkUtils.as_or_null(named_content_item, "InkContainer")
if named_sub_container:
named_sub_container.name = key
named_only_content[key] = named_content_item
container.named_only_content = named_only_content
return container
# (Dictionary<String, Variant>) -> Choice
func jobject_to_choice(jobj: Dictionary) -> InkChoice:
var choice = InkChoice.new()
choice.text = str(jobj["text"])
choice.index = int(jobj["index"])
choice.source_path = str(jobj["originalChoicePath"])
choice.original_thread_index = int(jobj["originalThreadIndex"])
choice.path_string_on_choice = str(jobj["targetPath"])
return choice
# (self.Json.Writer, Choice) -> Void
func write_choice(writer, choice: InkChoice) -> void:
writer.write_object_start()
writer.write_property("text", choice.text)
writer.write_property("index", choice.index)
writer.write_property("originalChoicePath", choice.source_path)
writer.write_property("originalThreadIndex", choice.original_thread_index)
writer.write_property("targetPath", choice.path_string_on_choice)
writer.write_object_end()
# (self.Json.Writer, ListValue) -> Void
func write_ink_list(writer, list_val):
var raw_list = list_val.value
writer.write_object_start()
writer.write_property_start("list")
writer.write_object_start()
for item_key in raw_list.raw_keys():
var item = InkListItem.from_serialized_key(item_key)
var item_val = raw_list.get_raw(item_key)
writer.write_property_name_start()
writer.write_property_name_inner(item.origin_name if item.origin_name else "?")
writer.write_property_name_inner(".")
writer.write_property_name_inner(item.item_name)
writer.write_property_name_end()
writer.write(item_val)
writer.write_property_end()
writer.write_object_end()
writer.write_property_end()
if raw_list.size() == 0 && raw_list.origin_names != null && raw_list.origin_names.size() > 0:
writer.write_property_start("origins")
writer.write_array_start()
for name in raw_list.origin_names:
writer.write(name)
writer.write_array_end()
writer.write_property_end()
writer.write_object_end()
# (ListDefinitionsOrigin) -> Dictionary<String, Variant>
func list_definitions_to_jtoken (origin):
var result = {} # Dictionary<String, Variant>
for def in origin.lists:
var list_def_json = {} # Dictionary<String, Variant>
for item_to_val_key in def.items:
var item = InkListItem.from_serialized_key(item_to_val_key)
var val = def.items[item_to_val_key]
list_def_json[item.item_name] = val
result[def.name] = list_def_json
return result
# (Variant) -> ListDefinitionsOrigin
func jtoken_to_list_definitions(obj):
var defs_obj = obj
var all_defs = [] # Array<ListDefinition>
for k in defs_obj:
var name = str(k) # String
var list_def_json = defs_obj[k] # Dictionary<String, Variant>
var items = {} # Dictionary<String, int>
for name_value_key in list_def_json:
items[name_value_key] = int(list_def_json[name_value_key])
var def = InkListDefinition.new(name, items)
all_defs.append(def)
return InkListDefinitionsOrigin.new(all_defs)
func _init(native_function_call):
_static_native_function_call = native_function_call
_control_command_names = []
_control_command_names.append("ev") # EVAL_START
_control_command_names.append("out") # EVAL_OUTPUT
_control_command_names.append("/ev") # EVAL_END
_control_command_names.append("du") # DUPLICATE
_control_command_names.append("pop") # POP_EVALUATED_VALUE
_control_command_names.append("~ret") # POP_FUNCTION
_control_command_names.append("->->") # POP_TUNNEL
_control_command_names.append("str") # BEGIN_STRING
_control_command_names.append("/str") # END_STRING
_control_command_names.append("nop") # NO_OP
_control_command_names.append("choiceCnt") # CHOICE_COUNT
_control_command_names.append("turn") # TURNS
_control_command_names.append("turns") # TURNS_SINCE
_control_command_names.append("readc") # READ_COUNT
_control_command_names.append("rnd") # RANDOM
_control_command_names.append("srnd") # SEED_RANDOM
_control_command_names.append("visit") # VISIT_INDEX
_control_command_names.append("seq") # SEQUENCE_SHUFFLE_INDEX
_control_command_names.append("thread") # START_THREAD
_control_command_names.append("done") # DONE
_control_command_names.append("end") # END
_control_command_names.append("listInt") # LIST_FROM_INT
_control_command_names.append("range") # LIST_RANGE
_control_command_names.append("lrnd") # LIST_RANDOM
_control_command_names.append("#") # BEGIN_TAG
_control_command_names.append("/#") # END_TAG
var i = 0
while i < InkControlCommand.CommandType.TOTAL_VALUES:
if _control_command_names[i] == null:
InkUtils.throw_exception("Control command not accounted for in serialisation")
i += 1
# Array<String>
var _control_command_names = null
# ############################################################################ #
# Eventually a pointer to InkRuntime.StaticJson
var _static_native_function_call = null

View file

@ -0,0 +1,284 @@
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends RefCounted
class_name InkStaticNativeFunctionCall
# ############################################################################ #
# Imports
# ############################################################################ #
const ValueType = preload("res://addons/inkgd/runtime/values/value_type.gd").ValueType
# ############################################################################ #
const ADD = "+"
const SUBTRACT = "-"
const DIVIDE = "/"
const MULTIPLY = "*"
const MOD = "%"
const NEGATE = "_"
const EQUALS = "=="
const GREATER = ">"
const LESS = "<"
const GREATER_THAN_OR_EQUALS = ">="
const LESS_THAN_OR_EQUALS = "<="
const NOT_EQUALS = "!="
const NOT = "!"
const AND = "&&"
const OR = "||"
const MIN = "MIN"
const MAX = "MAX"
const POW = "POW"
const FLOOR = "FLOOR"
const CEILING = "CEILING"
const INT = "INT"
const FLOAT = "FLOAT"
const HAS = "?"
const HASNT = "!?"
const INTERSECT = "^"
const LIST_MIN = "LIST_MIN"
const LIST_MAX = "LIST_MAX"
const ALL = "LIST_ALL"
const COUNT = "LIST_COUNT"
const VALUE_OF_LIST = "LIST_VALUE"
const INVERT = "LIST_INVERT"
# ############################################################################ #
var native_functions = null # Dictionary<String, String>
# ############################################################################ #
# (String) -> Bool
func call_exists_with_name(function_name):
generate_native_functions_if_necessary()
return native_functions.has(function_name)
# () -> void
func generate_native_functions_if_necessary():
if native_functions == null:
native_functions = {}
add_int_binary_op(ADD, "int_binary_op_add")
add_int_binary_op(SUBTRACT, "int_binary_op_substract")
add_int_binary_op(MULTIPLY, "int_binary_op_multiply")
add_int_binary_op(DIVIDE, "int_binary_op_divide")
add_int_binary_op(MOD, "int_binary_op_mod")
add_int_unary_op (NEGATE, "int_unary_op_negate")
add_int_binary_op(EQUALS, "int_binary_op_equals")
add_int_binary_op(GREATER, "int_binary_op_greater")
add_int_binary_op(LESS, "int_binary_op_less")
add_int_binary_op(GREATER_THAN_OR_EQUALS, "int_binary_op_greater_than_or_equals")
add_int_binary_op(LESS_THAN_OR_EQUALS, "int_binary_op_less_than_or_equals")
add_int_binary_op(NOT_EQUALS, "int_binary_op_not_equals")
add_int_unary_op (NOT, "int_unary_op_not")
add_int_binary_op(AND, "int_binary_op_and")
add_int_binary_op(OR, "int_binary_op_or")
add_int_binary_op(MAX, "int_binary_op_max")
add_int_binary_op(MIN, "int_binary_op_min")
add_int_binary_op(POW, "int_binary_op_pow")
add_int_unary_op (FLOOR, "int_unary_op_floor")
add_int_unary_op (CEILING, "int_unary_op_ceiling")
add_int_unary_op (INT, "int_unary_op_int")
add_int_unary_op (FLOAT, "int_unary_op_float")
add_float_binary_op(ADD, "float_binary_op_add")
add_float_binary_op(SUBTRACT, "float_binary_op_substract")
add_float_binary_op(MULTIPLY, "float_binary_op_multiply")
add_float_binary_op(DIVIDE, "float_binary_op_divide")
add_float_binary_op(MOD, "float_binary_op_mod")
add_float_unary_op (NEGATE, "float_unary_op_negate")
add_float_binary_op(EQUALS, "float_binary_op_equals")
add_float_binary_op(GREATER, "float_binary_op_greater")
add_float_binary_op(LESS, "float_binary_op_less")
add_float_binary_op(GREATER_THAN_OR_EQUALS, "float_binary_op_greater_than_or_equals")
add_float_binary_op(LESS_THAN_OR_EQUALS, "float_binary_op_less_than_or_equals")
add_float_binary_op(NOT_EQUALS, "float_binary_op_not_equals")
add_float_unary_op (NOT, "float_unary_op_not")
add_float_binary_op(AND, "float_binary_op_and")
add_float_binary_op(OR, "float_binary_op_or")
add_float_binary_op(MAX, "float_binary_op_max")
add_float_binary_op(MIN, "float_binary_op_min")
add_float_binary_op(POW, "float_binary_op_pow")
add_float_unary_op (FLOOR, "float_unary_op_floor")
add_float_unary_op (CEILING, "float_unary_op_ceiling")
add_float_unary_op (INT, "float_unary_op_int")
add_float_unary_op (FLOAT, "float_unary_op_float")
add_string_binary_op(ADD, "string_binary_op_add")
add_string_binary_op(EQUALS, "string_binary_op_equals")
add_string_binary_op(NOT_EQUALS, "string_binary_op_not_equals")
add_string_binary_op(HAS, "string_binary_op_has")
add_string_binary_op(HASNT, "string_binary_op_hasnt")
add_list_binary_op (ADD, "list_binary_op_add")
add_list_binary_op (SUBTRACT, "list_binary_op_substract")
add_list_binary_op (HAS, "list_binary_op_has")
add_list_binary_op (HASNT, "list_binary_op_hasnt")
add_list_binary_op (INTERSECT, "list_binary_op_intersect")
add_list_binary_op (EQUALS, "list_binary_op_equals")
add_list_binary_op (GREATER, "list_binary_op_greater")
add_list_binary_op (LESS, "list_binary_op_less")
add_list_binary_op (GREATER_THAN_OR_EQUALS, "list_binary_op_greater_than_or_equals")
add_list_binary_op (LESS_THAN_OR_EQUALS, "list_binary_op_less_than_or_equals")
add_list_binary_op (NOT_EQUALS, "list_binary_op_not_equals")
add_list_binary_op (AND, "list_binary_op_and")
add_list_binary_op (OR, "list_binary_op_or")
add_list_unary_op (NOT, "list_unary_op_not")
add_list_unary_op (INVERT, "list_unary_op_invert")
add_list_unary_op (ALL, "list_unary_op_all")
add_list_unary_op (LIST_MIN, "list_unary_op_list_min")
add_list_unary_op (LIST_MAX, "list_unary_op_list_max")
add_list_unary_op (COUNT, "list_unary_op_count")
add_list_unary_op (VALUE_OF_LIST, "list_unary_op_value_of_list")
add_op_to_native_func(EQUALS, 2, ValueType.DIVERT_TARGET,
"native_func_divert_targets_equal")
add_op_to_native_func(NOT_EQUALS, 2, ValueType.DIVERT_TARGET,
"native_func_divert_targets_not_equal")
# (String, int, ValueType, Variant)
func add_op_to_native_func(name, args, val_type, op):
var native_func = null # NativeFunctionCall
if native_functions.has(name):
native_func = native_functions[name]
else:
native_func = InkNativeFunctionCall.new_with_name_and_number_of_parameters(name, args, self)
native_functions[name] = native_func
native_func.add_op_func_for_type(val_type, op)
func add_int_binary_op(name, op_function_name):
add_op_to_native_func(name, 2, ValueType.INT, op_function_name)
func add_int_unary_op(name, op_function_name):
add_op_to_native_func(name, 1, ValueType.INT, op_function_name)
func add_float_binary_op(name, op_function_name):
add_op_to_native_func(name, 2, ValueType.FLOAT, op_function_name)
func add_float_unary_op(name, op_function_name):
add_op_to_native_func(name, 1, ValueType.FLOAT, op_function_name)
func add_string_binary_op(name, op_function_name):
add_op_to_native_func(name, 2, ValueType.STRING, op_function_name)
func add_list_binary_op(name, op_function_name):
add_op_to_native_func(name, 2, ValueType.LIST, op_function_name)
func add_list_unary_op(name, op_function_name):
add_op_to_native_func(name, 1, ValueType.LIST, op_function_name)
# ############################################################################ #
func int_binary_op_add(x, y): return x + y
func int_binary_op_substract(x, y): return x - y
func int_binary_op_multiply(x, y): return x * y
func int_binary_op_divide(x, y): return x / y
func int_binary_op_mod(x, y): return x % y
func int_unary_op_negate(x): return -x
func int_binary_op_equals(x, y): return x == y
func int_binary_op_greater(x, y): return x > y
func int_binary_op_less(x, y): return x < y
func int_binary_op_greater_than_or_equals(x, y): return x >= y
func int_binary_op_less_than_or_equals(x, y): return x <= y
func int_binary_op_not_equals(x, y): return x != y
func int_unary_op_not(x): return x == 0
func int_binary_op_and(x, y): return x != 0 && y != 0
func int_binary_op_or(x, y): return x != 0 || y != 0
func int_binary_op_max(x, y): return max(x, y)
func int_binary_op_min(x, y): return min(x, y)
func int_binary_op_pow(x, y): return pow(float(x), float(y))
func int_unary_op_floor(x): return x
func int_unary_op_ceiling(x): return x
func int_unary_op_int(x): return x
func int_unary_op_float(x): return float(x)
func float_binary_op_add(x, y): return x + y
func float_binary_op_substract(x, y): return x - y
func float_binary_op_multiply(x, y): return x * y
func float_binary_op_divide(x, y): return x / y
func float_binary_op_mod(x, y): return fmod(x, y)
func float_unary_op_negate(x): return -x
func float_binary_op_equals(x, y): return x == y
func float_binary_op_greater(x, y): return x > y
func float_binary_op_less(x, y): return x < y
func float_binary_op_greater_than_or_equals(x, y): return x >= y
func float_binary_op_less_than_or_equals(x, y): return x <= y
func float_binary_op_not_equals(x, y): return x != y
func float_unary_op_not(x): return x == 0.0
func float_binary_op_and(x, y): return x != 0.0 && y != 0.0
func float_binary_op_or(x, y): return x != 0.0 || y != 0.0
func float_binary_op_max(x, y): return max(x, y)
func float_binary_op_min(x, y): return min(x, y)
func float_binary_op_pow(x, y): return pow(x, y)
func float_unary_op_floor(x): return floor(x)
func float_unary_op_ceiling(x): return ceil(x)
func float_unary_op_int(x): return int(x)
func float_unary_op_float(x): return x
func string_binary_op_add(x, y): return str(x, y)
func string_binary_op_equals(x, y): return x == y
func string_binary_op_not_equals(x, y): return x != y
# Note: The Content Test (in) operator does not returns true when testing
# against the empty string, unlike the behaviour of the original C# runtime.
func string_binary_op_has(x, y): return y == "" || (y in x)
func string_binary_op_hasnt(x, y): return !(y in x) && y != ""
func list_binary_op_add(x, y): return x.union(y)
func list_binary_op_substract(x, y): return x.without(y)
func list_binary_op_has(x, y): return x.contains(y)
func list_binary_op_hasnt(x, y): return !x.contains(y)
func list_binary_op_intersect(x, y): return x.intersection(y)
func list_binary_op_equals(x, y): return x.equals(y)
func list_binary_op_greater(x, y): return x.greater_than(y)
func list_binary_op_less(x, y): return x.less_than(y)
func list_binary_op_greater_than_or_equals(x, y): return x.greater_than_or_equals(y)
func list_binary_op_less_than_or_equals(x, y): return x.less_than_or_equals(y)
func list_binary_op_not_equals(x, y): return !x.equals(y)
func list_binary_op_and(x, y): return x.size() > 0 && y.size() > 0
func list_binary_op_or(x, y): return x.size() > 0 || y.size() > 0
func list_unary_op_not(x): return 1 if x.size() == 0 else 0
func list_unary_op_invert(x): return x.inverse
func list_unary_op_all(x): return x.all
func list_unary_op_list_min(x): return x.min_as_list()
func list_unary_op_list_max(x): return x.max_as_list()
func list_unary_op_count(x): return x.size()
func list_unary_op_value_of_list(x): return x.max_item.value
func native_func_divert_targets_equal(d1, d2): return d1.equals(d2)
func native_func_divert_targets_not_equal(d1, d2): return !d1.equals(d2)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,125 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
# ############################################################################ #
# !! VALUE TYPE
# ############################################################################ #
# Pointers are passed around a lot, to prevent duplicating them all the time
# and confusing the inspector when the debugger is attached, they are
# immutable rather than being duplicated.
extends InkBase
class_name InkPointer
# ############################################################################ #
# InkContainer
# Encapsulating container into a weak ref.
var container: InkContainer:
get:
return self._container.get_ref()
set(value):
assert(false, "Pointer is immutable, cannot set container.")
var _container: WeakRef = WeakRef.new()
var index: int:
get:
return _index
set(value):
assert(false, "Pointer is immutable, cannot set index.")
var _index: int = 0 # int
# (InkContainer, int) -> InkPointer
func _init(container: InkContainer = null, index: int = 0):
if container == null:
self._container = WeakRef.new()
else:
self._container = weakref(container)
self._index = index
# () -> InkContainer
func resolve():
if self.index < 0: return self.container
if self.container == null: return null
if self.container.content.size() == 0: return self.container
if self.index >= self.container.content.size(): return null
return self.container.content[self.index]
# ############################################################################ #
# () -> bool
var is_null: bool: get = get_is_null
func get_is_null() -> bool:
return self.container == null
# ############################################################################ #
# TODO: Make inspectable
# () -> InkPath
var path: InkPath:
get:
if self.is_null:
return null
if self.index >= 0:
return self.container.path.path_by_appending_component(
InkPath.Component.new(self.index)
)
else:
return self.container.path
############################################################################# #
func _to_string() -> String:
if self.container == null:
return "Ink Pointer (null)"
return "Ink Pointer -> %s -- index %d" % [self.container.path._to_string(), self.index]
# (InkContainer) -> InkPointer
static func start_of(container: InkContainer) -> InkPointer:
return InkPointer.new(container, 0)
# ############################################################################ #
# () -> InkPointer
static var null_pointer: InkPointer:
get: return InkPointer.new(null, -1)
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "InkPointer" || super.is_ink_class(type)
func get_ink_class() -> String:
return "InkPointer"
func duplicate() -> InkPointer:
return InkPointer.new(self.container, self.index)

View file

@ -0,0 +1,62 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkValue
class_name InkBoolValue
# ############################################################################ #
func get_value_type() -> int:
return ValueType.BOOL
func get_is_truthy() -> bool:
return value
func _init():
value = false
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func cast(new_type, metadata = null):
if new_type == self.value_type:
return self
if new_type == ValueType.INT:
return IntValue().new_with(1 if value else 0)
if new_type == ValueType.FLOAT:
return FloatValue().new_with(1.0 if value else 0.0)
if new_type == ValueType.STRING:
return StringValue().new_with("true" if value else "false")
InkUtils.throw_story_exception(bad_cast_exception_message(new_type), false, metadata)
return null
func _to_string() -> String:
return "true" if value else "false"
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "BoolValue" || super.is_ink_class(type)
func get_ink_class():
return "BoolValue"
static func new_with(val):
var value = BoolValue().new()
value._init_with(val)
return value

View file

@ -0,0 +1,60 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkValue
class_name InkDivertTargetValue
# ############################################################################ #
var target_path : get = get_target_path, set = set_target_path # InkPath
func get_target_path():
return value
func set_target_path(value):
self.value = value
func get_value_type():
return ValueType.DIVERT_TARGET
func get_is_truthy():
InkUtils.throw_exception("Shouldn't be checking the truthiness of a divert target")
return false
func _init():
value = null
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func cast(new_type, metadata = null):
if new_type == self.value_type:
return self
InkUtils.throw_story_exception(bad_cast_exception_message(new_type), false, metadata)
return null
func _to_string() -> String:
return "DivertTargetValue(" + self.target_path._to_string() + ")"
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "DivertTargetValue" || super.is_ink_class(type)
func get_ink_class():
return "DivertTargetValue"
static func new_with(val):
var value = DivertTargetValue().new()
value._init_with(val)
return value

View file

@ -0,0 +1,59 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkValue
class_name InkFloatValue
# ############################################################################ #
func get_value_type():
return ValueType.FLOAT
func get_is_truthy():
return value != 0.0
func _init():
value = 0.0
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func cast(new_type, metadata = null):
if new_type == self.value_type:
return self
if new_type == ValueType.BOOL:
return BoolValue().new_with(false if value == 0 else true)
if new_type == ValueType.INT:
return IntValue().new_with(int(value))
if new_type == ValueType.STRING:
return StringValue().new_with(str(value)) # TODO: Check formating
InkUtils.throw_story_exception(bad_cast_exception_message(new_type), false, metadata)
return null
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "FloatValue" || super.is_ink_class(type)
func get_ink_class():
return "FloatValue"
static func new_with(val):
var value = FloatValue().new()
value._init_with(val)
return value

View file

@ -0,0 +1,59 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkValue
class_name InkIntValue
# ############################################################################ #
func get_value_type():
return ValueType.INT
func get_is_truthy():
return value != 0
func _init():
value = 0
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func cast(new_type, metadata = null):
if new_type == self.value_type:
return self
if new_type == ValueType.BOOL:
return BoolValue().new_with(false if value == 0 else true)
if new_type == ValueType.FLOAT:
return FloatValue().new_with(float(value))
if new_type == ValueType.STRING:
return StringValue().new_with(str(value))
InkUtils.throw_story_exception(bad_cast_exception_message(new_type), false, metadata)
return null
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "IntValue" || super.is_ink_class(type)
func get_ink_class():
return "IntValue"
static func new_with(val):
var value = IntValue().new()
value._init_with(val)
return value

View file

@ -0,0 +1,90 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkValue
class_name InkListValue
# ############################################################################ #
func get_value_type():
return ValueType.LIST
func get_is_truthy():
return value.size() > 0
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func cast(new_type, metadata = null):
if new_type == ValueType.INT:
var max_item = value.max_item
if max_item.key.is_null:
return IntValue().new_with(0)
else:
return IntValue().new_with(max_item.value)
elif new_type == ValueType.FLOAT:
var max_item = value.max_item
if max_item.key.is_null:
return FloatValue().new_with(0.0)
else:
return FloatValue().new_with(float(max_item.value))
elif new_type == ValueType.STRING:
var max_item = value.max_item
if max_item.key.is_null:
return StringValue().new_with("")
else:
return StringValue().new_with(max_item.key._to_string())
if new_type == self.value_type:
return self
InkUtils.throw_story_exception(bad_cast_exception_message(new_type), false, metadata)
return null
func _init():
value = InkList.new()
func _init_with_list(list):
value = InkList.new_with_ink_list(list)
func _init_with_single_item(single_item, single_value):
value = InkList.new_with_single_item(single_item, single_value)
# (InkObject, InkObject) -> void
static func retain_list_origins_for_assignment(old_value, new_value):
var old_list = InkUtils.as_or_null(old_value, "ListValue")
var new_list = InkUtils.as_or_null(new_value, "ListValue")
if old_list && new_list && new_list.value.size() == 0:
new_list.value.set_initial_origin_names(old_list.value.origin_names)
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type):
return type == "ListValue" || super.is_ink_class(type)
func get_ink_class():
return "ListValue"
static func new_with(list):
var value = ListValue().new()
value._init_with_list(list)
return value
static func new_with_single_item(single_item, single_value):
var value = ListValue().new()
value._init_with_single_item(single_item, single_value)
return value

View file

@ -0,0 +1,82 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkValue
class_name InkStringValue
# ############################################################################ #
func get_value_type():
return ValueType.STRING
func get_is_truthy():
return value.length() > 0
var is_newline: bool
var is_inline_whitespace: bool
var is_non_whitespace: bool:
get:
return !is_newline && !is_inline_whitespace
func _init():
value = ""
self._sanitize_value()
func _init_with(val):
super._init_with(val)
self._sanitize_value()
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func cast(new_type, metadata = null):
if new_type == self.value_type:
return self
if new_type == ValueType.INT:
if self.value.is_valid_int():
return IntValue().new_with(int(self.value))
else:
return null
if new_type == ValueType.FLOAT:
if self.value.is_valid_float():
return FloatValue().new_with(float(self.value))
else:
return null
InkUtils.throw_story_exception(bad_cast_exception_message(new_type), false, metadata)
return null
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "StringValue" || super.is_ink_class(type)
func get_ink_class():
return "StringValue"
func _sanitize_value():
is_newline = (self.value == "\n")
is_inline_whitespace = true
for c in self.value:
if c != ' ' && c != "\t":
is_inline_whitespace = false
break
static func new_with(val):
var value = StringValue().new()
value._init_with(val)
return value

View file

@ -0,0 +1,131 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkObject
# This is a merge of the original Value class and its Value<T> subclass.
class_name InkValue
# ############################################################################ #
# IMPORTS
# ############################################################################ #
const ValueType = preload("res://addons/inkgd/runtime/values/value_type.gd").ValueType
var InkList = load("res://addons/inkgd/runtime/lists/ink_list.gd")
# ############################################################################ #
# STATIC REFERENCE
# ############################################################################ #
# TODO: Remove
#static func Utils():
# return load("res://addons/inkgd/runtime/extra/InkUtils.gd")
#
#static func Value():
# return load("res://addons/inkgd/runtime/values/value.gd")
static func BoolValue():
return load("res://addons/inkgd/runtime/values/bool_value.gd")
static func IntValue():
return load("res://addons/inkgd/runtime/values/int_value.gd")
static func FloatValue():
return load("res://addons/inkgd/runtime/values/float_value.gd")
static func StringValue():
return load("res://addons/inkgd/runtime/values/string_value.gd")
static func DivertTargetValue():
return load("res://addons/inkgd/runtime/values/divert_target_value.gd")
static func VariablePointerValue():
return load("res://addons/inkgd/runtime/values/variable_pointer_value.gd")
static func ListValue():
return load("res://addons/inkgd/runtime/values/list_value.gd")
# ############################################################################ #
var value # Variant
# ValueType
var value_type: int: get = get_value_type
func get_value_type() -> int:
return -1
var is_truthy: bool: get = get_is_truthy
func get_is_truthy() -> bool:
return false
# ############################################################################ #
# (ValueType) -> ValueType
func cast(new_type: int) -> InkValue:
return null
var value_object: # Variant
get: return value
# ############################################################################ #
# (Variant) -> Value
func _init_with(val):
value = val
# (Variant) -> Value
static func create(val) -> InkValue:
# Original code lost precision from double to float.
# But it's not applicable here.
if val is bool:
return BoolValue().new_with(val)
if val is int:
return IntValue().new_with(val)
elif val is float:
return FloatValue().new_with(val)
elif val is String:
return StringValue().new_with(val)
elif InkUtils.is_ink_class(val, "InkPath"):
return DivertTargetValue().new_with(val)
elif InkUtils.is_ink_class(val, "InkList"):
return ListValue().new_with(val)
return null
func copy() -> InkValue:
return create(self.value_object)
# (Ink.ValueType) -> StoryException
func bad_cast_exception_message(target_ink_class) -> String:
return "Can't cast " + self.value_object + " from " + self.value_type + " to " + target_ink_class
# () -> String
func _to_string() -> String:
if value is int || value is float || value is String:
return str(value)
else:
return value._to_string()
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type) -> bool:
return type == "Value" || super.is_ink_class(type)
func get_ink_class() -> String:
return "Value"
static func new_with(val) -> InkValue:
var value = InkValue.new()
value._init_with(val)
return value

View file

@ -0,0 +1,23 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
enum ValueType {
BOOL = -1,
INT,
FLOAT,
LIST,
STRING,
DIVERT_TARGET,
VARIABLE_POINTER
}

View file

@ -0,0 +1,69 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkValue
class_name InkVariablePointerValue
# ############################################################################ #
var variable_name : get = get_variable_name, set = set_variable_name # InkPath
func get_variable_name():
return value
func set_variable_name(value):
self.value = value
func get_value_type():
return ValueType.VARIABLE_POINTER
func get_is_truthy():
InkUtils.throw_exception("Shouldn't be checking the truthiness of a variable pointer")
return false
var context_index = 0 # int
func _init_with_context(variable_name, context_index = -1):
super._init_with(variable_name)
self.context_index = context_index
func _init():
value = null
# The method takes a `StoryErrorMetadata` object as a parameter that
# doesn't exist in upstream. The metadat are used in case an 'exception'
# is raised. For more information, see story.gd.
func cast(new_type, metadata = null):
if new_type == self.value_type:
return self
InkUtils.throw_story_exception(bad_cast_exception_message(new_type), false, metadata)
return null
func _to_string() -> String:
return "VariablePointerValue(" + self.variable_name + ")"
func copy():
return VariablePointerValue().new_with_context(self.variable_name, context_index)
# ######################################################################## #
# GDScript extra methods
# ######################################################################## #
func is_ink_class(type):
return type == "VariablePointerValue" || super.is_ink_class(type)
func get_ink_class():
return "VariablePointerValue"
static func new_with_context(variable_name, context_index = -1):
var value = VariablePointerValue().new()
value._init_with_context(variable_name, context_index)
return value

View file

@ -0,0 +1,373 @@
# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# warning-ignore-all:return_value_discarded
# ############################################################################ #
# Copyright © 2015-2021 inkle Ltd.
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
# All Rights Reserved
#
# This file is part of inkgd.
# inkgd is licensed under the terms of the MIT license.
# ############################################################################ #
extends InkBase
class_name InkVariablesState
# ############################################################################ #
# Imports
# ############################################################################ #
var InkTryGetResult := preload("res://addons/inkgd/runtime/extra/try_get_result.gd") as GDScript
var InkStringSet := preload("res://addons/inkgd/runtime/extra/string_set.gd") as GDScript
var InkValue := load("res://addons/inkgd/runtime/values/value.gd") as GDScript
var InkListValue := load("res://addons/inkgd/runtime/values/list_value.gd") as GDScript
var InkVariablePointerValue := load("res://addons/inkgd/runtime/values/variable_pointer_value.gd") as GDScript
# ############################################################################ #
# (String, InkObject)
signal variable_changed(variable_name, new_value)
# ############################################################################ #
var patch: InkStatePatch # StatePatch
var batch_observing_variable_changes: bool:
get:
return _batch_observing_variable_changes
set(value):
_batch_observing_variable_changes = value
if value:
_changed_variables_for_batch_obs = InkStringSet.new()
else:
if _changed_variables_for_batch_obs != null:
for variable_name in _changed_variables_for_batch_obs.enumerate():
var current_value = _global_variables[variable_name]
emit_signal("variable_changed", variable_name, current_value)
_changed_variables_for_batch_obs = null
var _batch_observing_variable_changes: bool = false
var callstack: InkCallStack: get = get_callstack, set = set_callstack
func get_callstack() -> InkCallStack:
return _callstack
func set_callstack(value: InkCallStack):
_callstack = value
# (String) -> Variant
func get_variable(variable_name: String):
if self.patch != null:
var global: InkTryGetResult = patch.try_get_global(variable_name)
if global.exists:
return global.result.value_object
if _global_variables.has(variable_name):
return _global_variables[variable_name].value_object
elif _default_global_variables.has(variable_name):
return _default_global_variables[variable_name].value_object
else:
return null
# (String, Variant) -> void
func set_variable(variable_name: String, value) -> void:
if !_default_global_variables.has(variable_name):
InkUtils.throw_story_exception(
"Cannot assign to a variable (%s) that hasn't been declared in the story" \
% variable_name
)
return
var val: InkValue = InkValue.create(value)
if val == null:
if value == null:
InkUtils.throw_exception("Cannot pass null to VariableState")
else:
InkUtils.throw_exception("Invalid value passed to VariableState: %s" % str(value))
return
set_global(variable_name, val)
func enumerate() -> Array:
return _global_variables.keys()
func _init(callstack: InkCallStack, list_defs_origin: InkListDefinitionsOrigin, ink_runtime = null):
find_static_objects(ink_runtime)
_global_variables = {}
_callstack = callstack
_list_defs_origin = list_defs_origin
# () -> void
func apply_patch() -> void:
for named_var_key in self.patch.globals:
_global_variables[named_var_key] = self.patch.globals[named_var_key]
if _changed_variables_for_batch_obs != null:
for name in self.patch.changed_variables.enumerate():
_changed_variables_for_batch_obs.append(name)
patch = null
# (Dictionary<string, Variant>) -> void
func set_json_token(jtoken: Dictionary) -> void:
_global_variables.clear()
for var_val_key in _default_global_variables:
if jtoken.has(var_val_key):
var loaded_token = jtoken[var_val_key]
_global_variables[var_val_key] = self.StaticJSON.jtoken_to_runtime_object(loaded_token)
else:
_global_variables[var_val_key] = _default_global_variables[var_val_key]
func write_json(writer: InkSimpleJSON.Writer) -> void:
writer.write_object_start()
for key in _global_variables:
var name: String = key
var val: InkObject = _global_variables[key]
if self._ink_runtime.dont_save_default_values:
if self._default_global_variables.has(name):
if runtime_objects_equal(val, self._default_global_variables[name]):
continue
writer.write_property_start(name)
self.StaticJSON.write_runtime_object(writer, val)
writer.write_property_end()
writer.write_object_end()
func runtime_objects_equal(obj1: InkObject, obj2: InkObject) -> bool:
if !InkUtils.are_of_same_type(obj1, obj2):
return false
var bool_val: InkBoolValue = InkUtils.as_or_null(obj1, "BoolValue")
if bool_val != null:
return bool_val.value == InkUtils.cast(obj2, "BoolValue").value
var int_val: InkIntValue = InkUtils.as_or_null(obj1, "IntValue")
if int_val != null:
return int_val.value == InkUtils.cast(obj2, "IntValue").value
var float_val: InkFloatValue = InkUtils.as_or_null(obj1, "FloatValue")
if float_val != null:
return float_val.value == InkUtils.cast(obj2, "FloatValue").value
var val1: InkValue = InkUtils.as_or_null(obj1, "Value")
var val2: InkValue = InkUtils.as_or_null(obj2, "Value")
if val1 != null:
if val1.value_object is Object && val2.value_object is Object:
return val1.value_object.equals(val2.value_object)
else:
return val1.value_object == val2.value_object
InkUtils.throw_exception(
"FastRoughDefinitelyEquals: Unsupported runtime object type: %s" \
% obj1.get_ink_class()
)
return false
func get_variable_with_name(name: String, context_index = -1) -> InkObject:
var var_value: InkObject = get_raw_variable_with_name(name, context_index)
var var_pointer: InkVariablePointerValue = InkUtils.as_or_null(var_value, "VariablePointerValue")
if var_pointer:
var_value = value_at_variable_pointer(var_pointer)
return var_value
# (String) -> { exists: bool, result: InkObject }
func try_get_default_variable_value(name: String) -> InkTryGetResult:
if _default_global_variables.has(name):
return InkTryGetResult.new(true, _default_global_variables[name])
else:
return InkTryGetResult.new(false, null)
func global_variable_exists_with_name(name: String) -> bool:
return (
_global_variables.has(name) ||
_default_global_variables != null && _default_global_variables.has(name)
)
func get_raw_variable_with_name(name: String, context_index: int) -> InkObject:
var var_value: InkObject = null
if context_index == 0 || context_index == -1:
if self.patch != null:
var try_result: InkTryGetResult = self.patch.try_get_global(name)
if try_result.exists: return try_result.result
if _global_variables.has(name):
return _global_variables[name]
if self._default_global_variables != null:
if self._default_global_variables.has(name):
return self._default_global_variables[name]
var list_item_value: InkListValue = _list_defs_origin.find_single_item_list_with_name(name)
if list_item_value:
return list_item_value
var_value = _callstack.get_temporary_variable_with_name(name, context_index)
return var_value
# (InkVariablePointerValue) -> InkObject
func value_at_variable_pointer(pointer: InkVariablePointerValue) -> InkObject:
return get_variable_with_name(pointer.variable_name, pointer.context_index)
# (InkVariableAssignment, InkObject) -> void
func assign(var_ass: InkVariableAssignment, value: InkObject) -> void:
var name: String = var_ass.variable_name
var context_index: int = -1
var set_global: bool = false
if var_ass.is_new_declaration:
set_global = var_ass.is_global
else:
set_global = global_variable_exists_with_name(name)
if var_ass.is_new_declaration:
var var_pointer: InkVariablePointerValue = InkUtils.as_or_null(value, "VariablePointerValue")
if var_pointer:
var fully_resolved_variable_pointer: InkObject = resolve_variable_pointer(var_pointer)
value = fully_resolved_variable_pointer
else:
var existing_pointer: InkVariablePointerValue = null # VariablePointerValue
var first_time: bool = true
while existing_pointer || first_time:
first_time = false
existing_pointer = InkUtils.as_or_null(
get_raw_variable_with_name(name, context_index),
"VariablePointerValue"
)
if existing_pointer:
name = existing_pointer.variable_name
context_index = existing_pointer.context_index
set_global = (context_index == 0)
if set_global:
set_global(name, value)
else:
_callstack.set_temporary_variable(name, value, var_ass.is_new_declaration, context_index)
# () -> void
func snapshot_default_globals():
_default_global_variables = _global_variables.duplicate()
# (InkObject, InkObject) -> void
func retain_list_origins_for_assignment(old_value, new_value) -> void:
var old_list: InkListValue = InkUtils.as_or_null(old_value, "ListValue")
var new_list: InkListValue = InkUtils.as_or_null(new_value, "ListValue")
if old_list && new_list && new_list.value.size() == 0:
new_list.value.set_initial_origin_names(old_list.value.origin_names)
# (String, InkObject) -> void
func set_global(variable_name: String, value: InkObject) -> void:
var old_value = null # InkObject
# Slightly different structure from upstream, since we can't use
# try_get_global in the conditional.
if patch != null:
var patch_value: InkTryGetResult = patch.try_get_global(variable_name)
if patch_value.exists:
old_value = patch_value.result
if old_value == null:
if self._global_variables.has(variable_name):
old_value = self._global_variables[variable_name]
InkListValue.retain_list_origins_for_assignment(old_value, value)
if patch != null:
self.patch.set_global(variable_name, value)
else:
self._global_variables[variable_name] = value
if !value.equals(old_value):
if _batch_observing_variable_changes:
if patch != null:
patch.add_changed_variable(variable_name)
elif self._changed_variables_for_batch_obs != null:
_changed_variables_for_batch_obs.append(variable_name)
else:
emit_signal("variable_changed", variable_name, value)
# (VariablePointerValue) -> VariablePointerValue
func resolve_variable_pointer(var_pointer: InkVariablePointerValue) -> InkVariablePointerValue:
var context_index: int = var_pointer.context_index
if context_index == -1:
context_index = get_context_index_of_variable_named(var_pointer.variable_name)
var value_of_variable_pointed_to = get_raw_variable_with_name(
var_pointer.variable_name, context_index
)
var double_redirection_pointer: InkVariablePointerValue = InkUtils.as_or_null(
value_of_variable_pointed_to, "VariablePointerValue"
)
if double_redirection_pointer:
return double_redirection_pointer
else:
return InkVariablePointerValue.new_with_context(var_pointer.variable_name, context_index)
# ############################################################################ #
# (String) -> int
func get_context_index_of_variable_named(var_name):
if global_variable_exists_with_name(var_name):
return 0
return _callstack.current_element_index
# Dictionary<String, InkObject>
var _global_variables: Dictionary
var _default_global_variables = null # Dictionary<String, InkObject>
var _callstack: InkCallStack
var _changed_variables_for_batch_obs: InkStringSet = null
var _list_defs_origin: InkListDefinitionsOrigin
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "VariableState" || super.is_ink_class(type)
func get_ink_class() -> String:
return "VariableState"
# ############################################################################ #
# Static Properties
# ############################################################################ #
var StaticJSON: InkStaticJSON:
get: return self._ink_runtime.json
var _ink_runtime:
get: return _weak_ink_runtime.get_ref()
var _weak_ink_runtime: WeakRef
func find_static_objects(ink_runtime = null):
if ink_runtime != null:
_weak_ink_runtime = weakref(ink_runtime)
return
var runtime = Engine.get_main_loop().root.get_node("__InkRuntime")
InkUtils.__assert__(
runtime != null,
"[InkVariableStates] Could not retrieve 'InkRuntime' singleton from the scene tree."
)
_weak_ink_runtime = weakref(runtime)

BIN
audio/Ambiente.ogg (Stored with Git LFS) Normal file

Binary file not shown.

19
audio/Ambiente.ogg.import Normal file
View file

@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://3hulnd5c5me5"
path="res://.godot/imported/Ambiente.ogg-d8268c600c8aca0de710e563231cd1c8.oggvorbisstr"
[deps]
source_file="res://audio/Ambiente.ogg"
dest_files=["res://.godot/imported/Ambiente.ogg-d8268c600c8aca0de710e563231cd1c8.oggvorbisstr"]
[params]
loop=true
loop_offset=0.0
bpm=0.0
beat_count=0
bar_beats=4

Some files were not shown because too many files have changed in this diff Show more