diff --git a/README.md b/README.md
index 232e600..596cdfb 100644
--- a/README.md
+++ b/README.md
@@ -1,62 +1,31 @@
# godot-ink
-**This project is heavily under construction as C# support in Godot is a WIP.**
+An [ink](https://github.com/inkle/ink) integration for [Godot Engine](https://github.com/godotengine/godot).
-[Ink](https://github.com/inkle/ink) integration for [Godot Engine](https://github.com/godotengine/godot).
-As C# supprt in Godot is still in its early stages, you might need to manually add the addon files to your `.csproj`.
-
-### Currently supported features:
-* Running an Ink story and branching with choice indexes
-* Saving and loading Ink state
-* Tags
-* Knot/Stitch jumping
-* Getting/Setting Ink variables (InkLists aren't supported yet)
-* Observing Ink variables (Inklists aren't supported yet)
-* External function bindings
-* Read/Visit counts
-
-### TODO:
-* Getting/Setting/Observing InkLists
-* On the fly Ink to JSON compilation
-
-## Troubleshooting
-
-If you're having trouble enabling the editor plugin, it's probably because the `.cs` files aren't compiling with your project. You can solve the issue by adding this `ItemGroup` to your `.csproj` file.
-
-```xml
-
-
-
-
-
- $(ProjectDir)/ink-engine-runtime.dll
- False
-
-
-```
-
-Depending on the version of Godot you're using, you might still have issues with the editor plugin.
-Do not worry, you don't actually need to enable it to use **godot-ink**. If you don't want to bother with extensive troubleshooting, all you have to do is attach `addons/paulloz.ink/Story.cs` to a node (or use it as a singleton). This node will become the `Story` node for the rest of this documentation.
-
-### Game export
+## How to use
-As your `.json` files aren't Godot resources, you'll need to manually tell the engine to include them in the exported package. You can read more about that in the [documentation](https://godot.readthedocs.io/en/latest/getting_started/workflow/export/exporting_projects.html?highlight=export#export-mode).
+When the plugin is properly loaded, you should be able to use the new ink panel to inspect your story.
-## How to use
+![](inspector_screenshot.png)
-You'll need to put `ink-engine-runtime.dll` at the root of your Godot project.
+You'll also see a new `ink` section in your project settings. If you want to be able to compile your .ink files on the fly you can input the path to the inklecate binary here.
+The last thing you'll need to do in order to get going is to put `ink-engine-runtime.dll` at the root of your Godot project.
-Everything revolves around the `Story` node.
+---
+Everything is handled in a `Story` node.
If nothing is specified, the **C#** usage is the same as the **GDScript** one.
### Loading the story
+First you should navigate to your `.json` or `.ink` file and import it as an `Ink story` in Godot. To do that, select the file in Godot, go to `Import`, select `Ink story` under `Import As:` and click `ReImport`.
+
+![](import_screenshot.png)
+
To load your story, you can:
-* Point the `InkFilePath` exported variable to the location of your JSON Ink file and check the `AutoLoadStory` checkbox in the inspector.
-* Point the `InkFilePath` exported variable to the location of your JSON Ink file (in the inspector or via a script) and call `story.LoadStory()`.
-* Call `story.LoadStory(path)` with path pointing to the location of your JSON Ink file.
+* Point the `InkFile` exported variable to your `.json`/`.ink` file and check the `AutoLoadStory` checkbox in the inspector.
+* Point the `InkFile` exported variable to your `.json`/`.ink` file (in the inspector or via a script) and call `story.LoadStory()`.
### Running the story and making choices
@@ -111,7 +80,8 @@ You get and set the json state by calling `.GetState()` and `.SetState(String)`.
story.SetState(story.GetState())
```
-Alternatively you can save and load directly from disk (either by passing a path or a file as argument) with `.LoadStateFromDisk` and `.SaveStateOnDisk`.
+Alternatively you can save and load directly from disk (either by passing a path or a file as argument) with `.LoadStateFromDisk` and `.SaveStateOnDisk`.
+When using a path, the default behaviour is to use the `user://` folder. You can bypass this by passing a full path to the functions (e.g. `res://my_dope_save_file.json`).
```GDScript
story.SaveStateOnDisk("save.json")
@@ -178,6 +148,29 @@ You can know how many times a knot/stitch has been visited with `.VisitCountPath
print(story.VisitCountPathString("mycoolknot.myradstitch"))
```
+## Troubleshooting
+
+If you're having trouble enabling the editor plugin, it's probably because the `.cs` files aren't compiling with your project. You can solve the issue by adding this `ItemGroup` to your `.csproj` file.
+
+```xml
+
+
+
+
+
+ $(ProjectDir)/ink-engine-runtime.dll
+ False
+
+
+```
+
+Depending on the version of Godot you're using, you might still have issues with the editor plugin.
+Do not worry, you don't actually need to enable it to use **godot-ink**. If you don't want to bother with extensive troubleshooting, all you have to do is attach `addons/paulloz.ink/Story.cs` to a node (or use it as a singleton). This node will become the `Story` node for the rest of this documentation.
+
+### TODO:
+* Getting/Setting/Observing InkLists
+* On the fly Ink to JSON compilation (works on Windows, need some tweaking for Linux and Mac OS support)
+
## License
**godot-ink** is released under MIT license (see the [LICENSE](/LICENSE) file for more information).
diff --git a/addons/paulloz.ink/InkDock.cs b/addons/paulloz.ink/InkDock.cs
index 26774c2..94579ae 100644
--- a/addons/paulloz.ink/InkDock.cs
+++ b/addons/paulloz.ink/InkDock.cs
@@ -11,23 +11,26 @@ public class InkDock : Control
private String currentFilePath;
private Node storyNode;
- private RichTextLabel storyText;
+ private VBoxContainer storyText;
private VBoxContainer storyChoices;
+
+ private ScrollBar scrollbar;
public override void _Ready()
{
- fileSelect = GetNode("VBoxContainer/GridContainer/OptionButton");
+ fileSelect = GetNode("Container/Top/OptionButton");
fileDialog = GetNode("FileDialog");
fileSelect.Connect("item_selected", this, nameof(onFileSelectItemSelected));
fileDialog.Connect("file_selected", this, nameof(onFileDialogFileSelected));
fileDialog.Connect("popup_hide", this, nameof(onFileDialogHide));
storyNode = GetNode("Story");
- storyNode.SetScript(ResourceLoader.Load("res://addons/paulloz.ink/Story.cs") as Script);
- storyNode.Connect(nameof(Story.InkContinued), this, nameof(onStoryContinued));
- storyNode.Connect(nameof(Story.InkChoices), this, nameof(onStoryChoices));
- storyText = GetNode("VBoxContainer/StoryText");
- storyChoices = GetNode("VBoxContainer/StoryChoices");
+ storyNode.SetScript(ResourceLoader.Load("res://addons/paulloz.ink/InkStory.cs") as Script);
+ storyNode.Connect(nameof(InkStory.InkChoices), this, nameof(onStoryChoices));
+ storyText = GetNode("Container/Bottom/Scroll/Margin/StoryText");
+ storyChoices = GetNode("Container/Bottom/StoryChoices");
+
+ scrollbar = this.GetNode("Container/Bottom/Scroll").GetVScrollbar();
}
private void resetFileSelectItems()
@@ -38,7 +41,18 @@ private void resetFileSelectItems()
private void resetStoryContent()
{
- storyText.Text = "";
+ this.removeAllStoryContent();
+ this.removeAllChoices();
+ }
+
+ private void removeAllStoryContent()
+ {
+ foreach (Node n in storyText.GetChildren())
+ storyText.RemoveChild(n);
+ }
+
+ private void removeAllChoices()
+ {
foreach (Node n in storyChoices.GetChildren())
storyChoices.RemoveChild(n);
}
@@ -75,21 +89,37 @@ private void onFileDialogHide()
else
{
fileSelect.Select(2);
- storyNode.Set("InkFilePath", currentFilePath.Remove(0, 6));
+ storyNode.Set("InkFile", ResourceLoader.Load(currentFilePath));
storyNode.Call("LoadStory");
+ resetStoryContent();
continueStoryMaximally();
}
}
- private void continueStoryMaximally()
+ private async void continueStoryMaximally()
{
while ((bool)storyNode.Get("CanContinue"))
- storyNode.Call("Continue");
+ {
+ try
+ {
+ storyNode.Call("Continue");
+ onStoryContinued(storyNode.Get("CurrentText") as String, new String[] { });
+ }
+ catch (Ink.Runtime.StoryException e)
+ {
+ onStoryContinued(e.ToString(), new String[] { });
+ }
+ }
+ await ToSignal(GetTree(), "idle_frame");
+ this.scrollbar.Value = this.scrollbar.MaxValue;
}
private void onStoryContinued(String text, String[] tags)
{
- storyText.Text = (storyText.Text + text).TrimStart(new char[] { ' ', '\n' });
+ Label newLine = new Label();
+ newLine.Autowrap = true;
+ newLine.Text = text.Trim(new char[] { ' ', '\n' });
+ this.storyText.AddChild(newLine);
}
private void onStoryChoices(String[] choices)
@@ -107,8 +137,9 @@ private void onStoryChoices(String[] choices)
private void clickChoice(int idx)
{
- resetStoryContent();
storyNode.Callv("ChooseChoiceIndex", new Godot.Collections.Array() { idx });
+ this.removeAllChoices();
+ this.storyText.AddChild(new HSeparator());
continueStoryMaximally();
}
}
diff --git a/addons/paulloz.ink/InkDock.tscn b/addons/paulloz.ink/InkDock.tscn
index 02eb98b..d515701 100755
--- a/addons/paulloz.ink/InkDock.tscn
+++ b/addons/paulloz.ink/InkDock.tscn
@@ -1,52 +1,82 @@
-[gd_scene load_steps=3 format=2]
+[gd_scene load_steps=4 format=2]
[ext_resource path="res://addons/paulloz.ink/InkDock.cs" type="Script" id=1]
-[ext_resource path="res://addons/paulloz.ink/Story.cs" type="Script" id=2]
+[ext_resource path="res://addons/paulloz.ink/InkStory.cs" type="Script" id=2]
+
+[sub_resource type="StyleBoxFlat" id=1]
+bg_color = Color( 0.137255, 0.160784, 0.184314, 1 )
[node name="Ink" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
+rect_min_size = Vector2( 0, 200 )
+size_flags_horizontal = 3
+size_flags_vertical = 3
script = ExtResource( 1 )
-[node name="VBoxContainer" type="VBoxContainer" parent="."]
+[node name="Container" type="VBoxContainer" parent="."]
anchor_right = 1.0
-margin_bottom = 24.0
+anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
-[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
+[node name="Top" type="HBoxContainer" parent="Container"]
+editor/display_folded = true
margin_right = 1024.0
margin_bottom = 20.0
-size_flags_horizontal = 3
-size_flags_vertical = 0
-columns = 2
-[node name="Label" type="Label" parent="VBoxContainer/GridContainer"]
+[node name="Label" type="Label" parent="Container/Top"]
margin_top = 3.0
margin_right = 40.0
margin_bottom = 17.0
text = "Story :"
-[node name="OptionButton" type="OptionButton" parent="VBoxContainer/GridContainer"]
+[node name="OptionButton" type="OptionButton" parent="Container/Top"]
margin_left = 44.0
margin_right = 85.0
margin_bottom = 20.0
items = [ "", null, false, 0, null, "Load", null, false, 1, null ]
selected = 0
-[node name="StoryText" type="RichTextLabel" parent="VBoxContainer"]
+[node name="Bottom" type="HBoxContainer" parent="Container"]
margin_top = 24.0
margin_right = 1024.0
-margin_bottom = 624.0
-rect_min_size = Vector2( 100, 600 )
+margin_bottom = 600.0
size_flags_horizontal = 3
size_flags_vertical = 3
-custom_colors/default_color = Color( 1, 1, 1, 1 )
-[node name="StoryChoices" type="VBoxContainer" parent="VBoxContainer"]
-margin_top = 628.0
+[node name="Scroll" type="ScrollContainer" parent="Container/Bottom"]
+margin_right = 820.0
+margin_bottom = 576.0
+size_flags_horizontal = 11
+size_flags_vertical = 3
+custom_styles/bg = SubResource( 1 )
+scroll_horizontal_enabled = false
+
+[node name="Margin" type="MarginContainer" parent="Container/Bottom/Scroll"]
+margin_right = 820.0
+margin_bottom = 576.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+custom_constants/margin_right = 10
+custom_constants/margin_top = 10
+custom_constants/margin_left = 10
+custom_constants/margin_bottom = 10
+
+[node name="StoryText" type="VBoxContainer" parent="Container/Bottom/Scroll/Margin"]
+margin_left = 10.0
+margin_top = 10.0
+margin_right = 810.0
+margin_bottom = 566.0
+size_flags_horizontal = 11
+size_flags_vertical = 3
+
+[node name="StoryChoices" type="VBoxContainer" parent="Container/Bottom"]
+margin_left = 824.0
margin_right = 1024.0
-margin_bottom = 628.0
+margin_bottom = 576.0
+rect_min_size = Vector2( 200, 0 )
+custom_constants/separation = 10
[node name="FileDialog" type="FileDialog" parent="."]
margin_right = 461.0
@@ -57,4 +87,3 @@ filters = PoolStringArray( "*.json" )
[node name="Story" type="Node" parent="."]
script = ExtResource( 2 )
-
diff --git a/addons/paulloz.ink/Story.cs b/addons/paulloz.ink/InkStory.cs
similarity index 85%
rename from addons/paulloz.ink/Story.cs
rename to addons/paulloz.ink/InkStory.cs
index 5a9263e..36a7892 100644
--- a/addons/paulloz.ink/Story.cs
+++ b/addons/paulloz.ink/InkStory.cs
@@ -5,7 +5,7 @@
#if TOOLS
[Tool]
#endif
-public class Story : Node
+public class InkStory : Node
{
// All the signals we'll need
[Signal] public delegate void InkContinued(String text, String[] tags);
@@ -20,7 +20,7 @@ private String ObservedVariableSignalName(String name)
// All the exported variables
[Export] public Boolean AutoLoadStory = false;
- [Export] public String InkFilePath = null;
+ [Export] public TextFile InkFile = null;
// All the public variables
public String CurrentText = "";
@@ -55,48 +55,24 @@ public override void _Ready()
this.LoadStory();
}
- public void LoadStory()
- {
- this.LoadStory(this.InkFilePath);
- }
-
- public Boolean LoadStory(String inkFilePath)
+ public Boolean LoadStory()
{
this.reset();
- try
- {
- if (inkFilePath == null)
- throw new System.IO.FileNotFoundException(String.Format("Unable to find {0}.", null));
-
- this.InkFilePath = inkFilePath;
-
- String path = this.InkFilePath.StartsWith("res://") ? this.InkFilePath : String.Format("res://{0}", this.InkFilePath);
- File file = new File();
- if (file.FileExists(path))
- {
- // Load the story
- file.Open(path, (int)File.ModeFlags.Read);
- this.story = new Ink.Runtime.Story(file.GetAsText());
- file.Close();
- }
- else
- throw new System.IO.FileNotFoundException(String.Format("Unable to find {0}.", path));
- }
- catch (System.IO.FileNotFoundException e)
- {
- GD.PrintErr(e.ToString());
-
+ if (!this.isJSONFileValid())
return false;
- }
+ this.story = new Ink.Runtime.Story(this.InkFile.GetMeta("content") as String);
return true;
}
- public void LoadStory(String inkFilePath, String state)
+ public Boolean LoadStory(String state)
{
- if (this.LoadStory(inkFilePath))
+ if (this.LoadStory())
this.SetState(state);
+ else
+ return false;
+ return true;
}
public String Continue()
@@ -275,4 +251,9 @@ public void LoadStateFromDisk(File file)
this.story.state.LoadJson(file.GetAsText());
}
}
+
+ private Boolean isJSONFileValid()
+ {
+ return this.InkFile != null && this.InkFile.HasMeta("content");
+ }
}
diff --git a/addons/paulloz.ink/PaullozDotInk.cs b/addons/paulloz.ink/PaullozDotInk.cs
index fc90126..12dc0f0 100644
--- a/addons/paulloz.ink/PaullozDotInk.cs
+++ b/addons/paulloz.ink/PaullozDotInk.cs
@@ -1,31 +1,68 @@
#if TOOLS
using Godot;
+using Godot.Collections;
using System;
[Tool]
public class PaullozDotInk : EditorPlugin
{
+ private Dictionary settings = new Dictionary() { {"inklecate_path", "---"} };
private const String addonBasePath = "res://addons/paulloz.ink";
- private NodePath customTypeScriptPath = $"{addonBasePath}/Story.cs";
+ private NodePath customTypeScriptPath = $"{addonBasePath}/InkStory.cs";
private NodePath customTypeIconPath = $"{addonBasePath}/icon.svg";
private NodePath dockScene = $"{addonBasePath}/InkDock.tscn";
private Control dock;
+ private NodePath importPluginScriptPath = $"{addonBasePath}/import_ink.gd";
+ private EditorImportPlugin importPlugin;
+
public override void _EnterTree()
{
- AddCustomType("Story", "Node", ResourceLoader.Load(customTypeScriptPath) as Script, ResourceLoader.Load(customTypeIconPath) as Texture);
+ // Settings
+ foreach (String key in settings.Keys)
+ {
+ String property_name = $"ink/{key}";
+ if (!ProjectSettings.HasSetting(property_name))
+ {
+ ProjectSettings.SetSetting(property_name, settings[key]);
+ ProjectSettings.SetInitialValue(property_name, settings[key]);
+ }
+ }
+ ProjectSettings.Save();
+
+ // Resources
+ importPlugin = (ResourceLoader.Load(importPluginScriptPath) as GDScript).New() as EditorImportPlugin;
+ AddImportPlugin(importPlugin);
+
+ // Custom types
+ AddCustomType("Ink Story", "Node", ResourceLoader.Load(customTypeScriptPath) as Script, ResourceLoader.Load(customTypeIconPath) as Texture);
+ // Editor
dock = (ResourceLoader.Load(dockScene) as PackedScene).Instance() as Control;
- AddControlToDock(EditorPlugin.DockSlot.RightUl, dock);
+ AddControlToBottomPanel(dock, "Ink");
}
public override void _ExitTree()
{
- RemoveControlFromDocks(dock);
+ // Editor
+ RemoveControlFromBottomPanel(dock);
+ dock.Free();
+
+ // Custom types
+ RemoveCustomType("Ink Story");
+
+ // Resources
+ RemoveImportPlugin(importPlugin);
- RemoveCustomType("Story");
+ // Settings
+ foreach (String key in settings.Keys)
+ {
+ String property_name = $"ink/{key}";
+ if (ProjectSettings.HasSetting(property_name))
+ ProjectSettings.SetSetting(property_name, null);
+ }
}
}
#endif
\ No newline at end of file
diff --git a/addons/paulloz.ink/icon.svg b/addons/paulloz.ink/icon.svg
index 3becfa4..dc29217 100644
--- a/addons/paulloz.ink/icon.svg
+++ b/addons/paulloz.ink/icon.svg
@@ -1,14 +1,7 @@
-
+
\ No newline at end of file
diff --git a/addons/paulloz.ink/import_ink.gd b/addons/paulloz.ink/import_ink.gd
new file mode 100644
index 0000000..376fef0
--- /dev/null
+++ b/addons/paulloz.ink/import_ink.gd
@@ -0,0 +1,72 @@
+tool
+extends EditorImportPlugin
+
+func get_importer_name():
+ return "ink";
+
+func get_visible_name():
+ return "Ink story";
+
+func get_recognized_extensions():
+ return [ "json", "ink" ];
+
+func get_save_extension():
+ return "res";
+
+func get_resource_type():
+ return "TextFile";
+
+func get_import_options(preset):
+ return []
+
+func get_preset_count():
+ return 0
+
+func import(source_file, save_path, options, r_platform_variants, r_gen_files):
+ match source_file.split(".")[-1].to_lower():
+ "ink":
+ return import_from_ink(source_file, save_path)
+ "json":
+ return import_from_json(source_file, save_path)
+
+func import_from_ink(source_file, save_path):
+ if ProjectSettings.has_setting("ink/inklecate_path"):
+ var inklecate = ProjectSettings.get_setting("ink/inklecate_path")
+ if inklecate != "---":
+ var new_file = "%d.json" % int(randf() * 100000)
+
+ var err = OS.execute(inklecate, [
+ "-o", "%s/%s" % [OS.get_user_data_dir(), new_file],
+ ProjectSettings.globalize_path(source_file)
+ ], true)
+
+ new_file = "user://%s" % new_file
+ if !File.new().file_exists(new_file):
+ return ERR_FILE_UNRECOGNIZED
+ var ret = import_from_json(new_file, save_path)
+
+ Directory.new().remove(new_file)
+ return ret
+
+func import_from_json(source_file, save_path):
+ var raw_content = get_source_file_content(source_file)
+
+ var parsed_content = parse_json(raw_content)
+ if !parsed_content.has("inkVersion"):
+ return ERR_FILE_UNRECOGNIZED
+
+ var resource = TextFile.new()
+ resource.set_meta("content", raw_content);
+
+ return ResourceSaver.save("%s.%s" % [save_path, get_save_extension()], resource)
+
+func get_source_file_content(source_file):
+ var file = File.new()
+ var err = file.open(source_file, File.READ)
+ if err != OK:
+ return err
+
+ var raw_content = file.get_as_text()
+
+ file.close()
+ return raw_content
\ No newline at end of file
diff --git a/import_screenshot.png b/import_screenshot.png
new file mode 100644
index 0000000..bb40015
Binary files /dev/null and b/import_screenshot.png differ
diff --git a/inspector_screenshot.png b/inspector_screenshot.png
new file mode 100644
index 0000000..3683f44
Binary files /dev/null and b/inspector_screenshot.png differ