From 1cae6b75cc2182a1714e634d13a06fe5853e25a4 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Sat, 27 Nov 2021 18:02:47 +0100 Subject: [PATCH 01/19] wip: started working on tutorial & relevant files --- Dialogue.gd | 10 ++- Dialogue.tscn | 14 +--- ExampleScene.tscn | 66 +++++++++++++++ README.md | 201 ++++++++++++++++++++++++++++++++++++++++++++-- Sally.yarn | 55 +++++++++++++ Ship.yarn | 34 ++++++++ TestScene.tscn | 20 +++++ 7 files changed, 379 insertions(+), 21 deletions(-) create mode 100644 ExampleScene.tscn create mode 100644 Sally.yarn create mode 100644 Ship.yarn create mode 100644 TestScene.tscn diff --git a/Dialogue.gd b/Dialogue.gd index 9aff8fd..4c243eb 100644 --- a/Dialogue.gd +++ b/Dialogue.gd @@ -4,6 +4,10 @@ func _ready(): $RichTextLabel/Logo.hide() $VBoxContainer/ButtonTemplate.hide() + $Wol.connect('line', self, '_on_line') + $Wol.connect('option', self, '_on_option') + $Wol.connect('finished', self, '_on_finished') + func continue_dialogue(): if $Tween.is_active(): $Tween.remove_all() @@ -12,7 +16,7 @@ func continue_dialogue(): $Wol.resume() -func _on_Wol_line(line): +func _on_line(line): $RichTextLabel.bbcode_text = line.text $Tween.remove_all() @@ -26,7 +30,7 @@ func _on_Wol_line(line): $Tween.start() -func _on_Wol_options(options): +func _on_options(options): var button_template = $VBoxContainer/ButtonTemplate for option in options: @@ -45,7 +49,7 @@ func _on_option_selected(option): if not 'Template' in child.name: child.queue_free() -func _on_Wol_finished(): +func _on_finished(): $RichTextLabel.text = '' func _input(event): diff --git a/Dialogue.tscn b/Dialogue.tscn index 6e2a4cd..3e1745d 100644 --- a/Dialogue.tscn +++ b/Dialogue.tscn @@ -1,6 +1,5 @@ -[gd_scene load_steps=12 format=2] +[gd_scene load_steps=11 format=2] -[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=1] [ext_resource path="res://addons/Wol/logo.svg" type="Texture" id=3] [ext_resource path="res://addons/Wol/font/Italic.tres" type="DynamicFont" id=4] [ext_resource path="res://addons/Wol/font/Regular.tres" type="DynamicFont" id=5] @@ -58,13 +57,6 @@ __meta__ = { "_edit_use_anchors_": false } -[node name="Wol" type="Node" parent="."] -script = ExtResource( 1 ) -path = "res://dialogue.yarn" -auto_start = true -variable_storage = { -} - [node name="Logo" type="TextureRect" parent="."] anchor_left = 1.0 anchor_top = 1.0 @@ -137,7 +129,3 @@ custom_styles/normal = SubResource( 1 ) text = "This is a dialogue option" [node name="Tween" type="Tween" parent="."] - -[connection signal="finished" from="Wol" to="." method="_on_Wol_finished"] -[connection signal="line" from="Wol" to="." method="_on_Wol_line"] -[connection signal="options" from="Wol" to="." method="_on_Wol_options"] diff --git a/ExampleScene.tscn b/ExampleScene.tscn new file mode 100644 index 0000000..492d74f --- /dev/null +++ b/ExampleScene.tscn @@ -0,0 +1,66 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=1] +[ext_resource path="res://Dialogue.tscn" type="PackedScene" id=2] + +[sub_resource type="RectangleShape2D" id=1] + +[node name="ExampleScene" type="Node2D"] + +[node name="Dialogue" parent="." instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 1024.0 +margin_bottom = 600.0 + +[node name="Wol" type="Node" parent="Dialogue"] +script = ExtResource( 1 ) +variable_storage = { +} + +[node name="Player" type="KinematicBody2D" parent="."] +position = Vector2( 360, 488 ) + +[node name="ColorRect" type="ColorRect" parent="Player"] +margin_left = -21.0 +margin_top = -52.0 +margin_right = 19.0 +margin_bottom = -12.0 +color = Color( 0, 0, 0, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect2" type="ColorRect" parent="Player"] +margin_left = -8.0 +margin_top = -47.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect4" type="ColorRect" parent="Player"] +margin_top = -23.0 +margin_right = 8.0 +margin_bottom = -19.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect3" type="ColorRect" parent="Player"] +margin_left = 7.0 +margin_top = -47.0 +margin_right = 15.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] +position = Vector2( -1, -9 ) +shape = SubResource( 1 ) + +[node name="StaticBody2D" type="StaticBody2D" parent="."] + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"] +polygon = PoolVector2Array( -3, 427, -40, 426, -40, 648, 838, 647, 838, 419, 787, 420, 788, 600, 2, 600, 0, 420 ) diff --git a/README.md b/README.md index a0a7299..bc19944 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ Wol is actively maintained by [Bram Dingelstad](https://bram.dingelstad.works), ## Getting Started -This repo contains the source code for the Wol compiler. If you want to use it in a game engine other than Godot, you should get the appropriate package for your game engine. Check out [YarnSpinner-Unity](https://github.com/YarnSpinnerTool/YarnSpinner-Unity) for a Unity version of this project. +This repo contains the code for the Wol nodes & Yarn compiler. +If you want to use it in a game engine other than Godot, you should get the appropriate package for your game engine. +Check out [YarnSpinner-Unity](https://github.com/YarnSpinnerTool/YarnSpinner-Unity) for a Unity version of this project. ### Download from AssetLib Unfortunately, this option isn't available yet. Stay tuned! @@ -38,9 +40,9 @@ There are few things that need to be ironed out to be 100% feature compatible wi - [ ] Full support for [format functions](https://yarnspinner.dev/docs/syntax/#format-functions). - [ ] In-editor dialogue editor with preview. - [ ] Fully extend the documentation of this project. + - [ ] Replicate the files needed for the Yarn Spinner tutorial. - [x] Document the [Option](README.md#Option) object. - [x] Write the method descriptions for the `Wol` node. - - [ ] Write a basic "Hello World"-esque tutorial. - [x] Provide helpful anchors in the documentation. - [x] Porting to usable signals in Godot. - [x] Providing helpful errors when failing to compile. @@ -207,7 +209,7 @@ It has several properties that you can change either in-editor or using GDScript When getting an option from the `options` signal, use this function to let Wol node which option you want to select. Use `Option.id` for the `id` parameter. -## [Line](README.md#Line) +## `Line` _Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_ An object holding all information related to a line in your dialogue. @@ -245,7 +247,7 @@ The [Line](README.md#Line) object is _the_ object that you're gonna be interacti Currently unimplemented. -## [Option](README.md#Option) +## `Option` _Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_ An object holding information of an option in your dialogue. @@ -278,4 +280,193 @@ It has a reference to a [Line](README.md#Line) with it's `line` property so you # Tutorial -_The tutorial is currently under construction, stay tuned!_ +Welcome to Yarn Spinner! In this tutorial, you’ll learn how to use Wol in a Godot project to create interactive dialogue. + +We’ll start by downloading and installing Yarn Spinner. We’ll then take a look at the core concepts that power Yarn Spinner, and write some dialogue. +After that, we’ll explore some of the more advanced features of Yarn Spinner. + +## Introducing Yarn Spinner + +Wol & Yarn (Spinner) are tools for writing interactive dialogue in games - that is, conversations that the player can have with characters in the game. +Yarn Spinner does this by letting you write your dialogue in a programming language called Yarn. + +Yarn is designed to be as minimal as possible. +For example, the following is valid Yarn code: + +```yarn +Gregg: I think I might be sick. +Mae: True friendship: Letting your friend make you sick. +Gregg: True bros. +Mae: True bros. +``` + +Wol will take each line, one at a time, and deliver them to the game. +It’s entirely up to the game to decide what to do with the lines; for example, the game that these lines are from, +Night in the Woods, displays them in speech bubbles, one character at a time, and waits for the user to press a button before showing the next one. + + +## Quick Start + +We’ll begin by playing the example game that comes with Wol. It’s very short - about 2 minutes long. After that we'll make some small changes! + +1. Create a new empty Godot project. +2. Download and install Wol. Go to the [Getting started section](#README.md#Getting-Started), and follow the directions there. +3. Open the example scene. +4. Play the game. Use the left and right arrow keys to move, and the space bar to talk to characters. + +We’re now ready to start looking under the hood, to see how Wol & Yarn power this game. + +### The Yarn Editor + +Wol & Yarn Spinner stores its dialogue in .yarn (or .wol) files. These are plain text files, which means you can edit them in any plain text editor (Visual Studio Code is a good option, and Secret Labs offers a syntax highlighting extension to make it nice to use!) + +You can also use the Wol Editor, which is a tool in the Godot editor for working with Yarn code. This editor is useful because it lets you view the structure of your dialogue in a very visual way. + +### Reading Yarn + +In this section of the tutorial, we’re going to open the file Sally.yarn, and look at what it’s doing. + +#### Open Sally.yarn in your editor of choice. + +Yarn Spinner groups all of its dialogue into nodes. Nodes contain everything: your lines of dialogue, the choices you show to the player, and the commands that you send to the game. The Sally.yarn file contains four of them: Sally, Sally.Watch, Sally.Exit, and Sally.Sorry. The example game is set up so that when you walk up to Sally and press the spacebar, the game will start running the Sally node. + +#### Go to the Sally node. + +Let’s take a look at what that node contains. Here’s the entire text of it: + +```yarn +<> + Player: Hey, Sally. #line:794945 + Sally: Oh! Hi. #line:2dc39b + Sally: You snuck up on me. #line:34de2f + Sally: Don't do that. #line:dcc2bc +<> + Player: Hey. #line:a8e70c + Sally: Hi. #line:305cde +<> + +<> + [[Anything exciting happen on your watch?|Sally.Watch]] #line:5d7a7c +<> + +<> + [[Sorry about the console.|Sally.Sorry]] #line:0a7e39 +<> +[[See you later.|Sally.Exit]] #line:0facf7 +``` + +Take a second now to look at this code, and get a feel for its structure. + +### Lines and Logic + +We’ll now take a closer look at each part of this code, and explain what’s going on. + +```yarn +<> + Player: Hey, Sally. #line:794945 + Sally: Oh! Hi. #line:2dc39b + Sally: You snuck up on me. #line:34de2f + Sally: Don't do that. #line:dcc2bc +<> + Player: Hey. #line:a8e70c + Sally: Hi. #line:305cde +<> +``` + +The first line of code in this node checks to see if Yarn Spinner has already run this node. visited is a function that this example game has defined - it isn’t built into Yarn Spinner. It returns true if the node you specify has been run before. You’ll notice that this line is wrapped in << and >> symbols. This tells Yarn Spinner that it’s control code, and not meant to be shown to the player. + +If they haven’t run the Sally node yet, it means that this is the first time that we’ve spoken to Sally in this game. As a result, we run lines in which Sally and the player character meet. Otherwise, we instead run some shorter lines. + +Each line in Yarn Spinner is just a run of text, which is sent directly to the game. It’s up to the game to decide how it wants to display it; in the example game, it’s shown at the top of the screen. + +At the end of each line, you’ll see a #line: tag. This tag lets Yarn Spinner identify lines across multiple translations, and is optional if you aren’t translating your game into other languages. Yarn Spinner can automatically generate them for you. +Options + +Here’s the next part of the code. + +```yarn +<> + [[Anything exciting happen on your watch?|Sally.Watch]] #line:5d7a7c +<> + +<> + [[Sorry about the console.|Sally.Sorry]] #line:0a7e39 +<> +``` + +In the next part of the code, we do a check, and if it passes, we add an option. Options are things that the player can select; in this game, they’re things the player can say, but like lines, it’s up to the game to decide what to do with them. Options are shown to the player when the end of a node is reached. + +The first couple of lines here test to see whether the player has run the node Sally.Watch. If they haven’t, then the code adds a new option. Options are wrapped with [[ and ]]. The text before the | is shown to the player, and the text after is the name of the node that will be run if the player chooses the option. Like lines, options can have line tags for localisation. + +If the player has run the Sally.Watch node before, this code won’t be run, which means that the option to run it again won’t appear. + +The rest of this part does a similar thing as the first: it does a check, and adds another option if the check passes. In this case, it checks to see if the variable $sally_warning is true, and if the player has not yet run the Sally.Sorry node. $sally_warning is set in a different node - it’s in the node Ship, which is stored in the file Ship.yarn. + +```yarn +[[See you later.|Sally.Exit]] #line:0facf7 +``` + +The very last line of the node adds an option, which takes the player to the Sally.Exit line. Because this option isn’t inside an if statement, it’s always added. + +When Yarn Spinner hits the end of the node, all of the options that have been accumulated so far will be shown to the player. Yarn Spinner will then wait for the player to make a selection, and then start running the node that they selected. + +And that’s how the node works! +Writing Some Dialogue + +Let’s write some dialogue! We’ll add a couple of lines to the Ship. + + Open the file Ship.yarn. It contains a single node, called Ship - go to it. + +This code uses couple of features that we didn’t see in Sally: commands, and variables. +Commands + +Commands are messages that Yarn Spinner sends to your game, but aren’t intended to be shown to the player. Commands let you control things in your scene, like moving the camera around, or instructing a character to move to another point. + +Because every game is different, Yarn Spinner leaves the task of defining most commands to you. Yarn Spinner defines two built-in commands: wait, which pauses the dialogue for a certain number of seconds, and stop, which ends the dialogue immediately. + +The example game defines its own command, setsprite, which is used to change the sprite that the Ship character’s face is displaying. You can see this in action in the file Ship.yarn: + +```yarn +Player: How's space? +Ship: Oh, man. +<> +Ship: It's HUGE! +<> +``` + +You can learn how to define your own custom commands in Working With Commands. +Variables + +Variables are how you store information about what the player has done in the game. We saw variables in use in the Sally node, where the variable $sally_warning was used to control whether some content was shown or not. This variable is set in here, in the Ship node - it represents whether or not the player has heard Sally’s warning about the console from the Ship. + +Variables in Yarn Spinner start with a $, and can store text, numbers, booleans (true or false values), or null. If you try and access a variable that hasn’t been set, you’ll get the value null, which represents “no value”. +Adding Some Content + +#### Add some new dialogue. Add the following text to the end of the node: + +``` +Ship: Anything else I can help with? + +-> No, thanks. + Ship: Aw, ok! +-> I'm good. + Ship: Let me know! + +Ship: Bye! +``` + +#### Shortcut Options + +The `->` items that we just added are called shortcut options. Shortcut options let you put choices in your node without having to create new nodes, which you link to through the [[Option]] syntax. They exist in-line with the rest of your node. + +To use a shortcut option, you write a `->`, followed by the text that you want to display. Then, on the next lines, indent the code a few spaces (it doesn’t matter how many, as long as you’re consistent.) The indented lines will run if the option they’re attached to is selected. Shortcut options can be nested, which means you can put a group of shortcut options inside another. You can put any kind of code inside a shortcut option’s lines. + +Because shortcut options don’t require you to create new nodes, they’re really good for situations where you want to offer the player some kind of choice that doesn’t significantly change the flow of the story. + +Save the file, and go back to the game. Play the game again, and talk to the Ship. At the end of the conversation, you’ll see new dialogue. + +### Where Next + +The example game is set up so that when you talk to Sally, the node Sally is run, and when you talk to the Ship, the node Ship is run. With this in mind, change the story so that after you get told off by Sally, she asks you to go and fix a problem with the Ship. + +You can also read the Syntax Reference for Yarn Spinner. diff --git a/Sally.yarn b/Sally.yarn new file mode 100644 index 0000000..0101a35 --- /dev/null +++ b/Sally.yarn @@ -0,0 +1,55 @@ +title: Sally +tags: +colorID: 0 +position: 524,111 +--- +<> + Player: Hey, Sally. #line:794945 + Sally: Oh! Hi. #line:2dc39b + Sally: You snuck up on me. #line:34de2f + Sally: Don't do that. #line:dcc2bc +<> + Player: Hey. #line:a8e70c + Sally: Hi. #line:305cde +<> + +<> + [[Anything exciting happen on your watch?|Sally.Watch]] #line:5d7a7c +<> + +<> + [[Sorry about the console.|Sally.Sorry]] #line:0a7e39 +<> +[[See you later.|Sally.Exit]] #line:0facf7 +=== + +title: Sally.Watch +tags: +colorID: 0 +position: 512,430 +--- +Sally: Not really. #line:8c3f98 +Sally: Same old nebula, doing the same old thing. #line:24c418 +Sally: Oh, Ship wanted to see you. Go say hi to it. #line:df4eaf +<> +<> + Player: Already done! #line:1fea6c + Sally: Go say hi again. #line:5df323 +<> +=== + +title: Sally.Exit +tags: +colorID: 6 +position: 211,417 +--- +Sally: Bye. #line:60c282 +=== + +title: Sally.Sorry +tags: +colorID: 0 +position: 827,439 +--- +Sally: Yeah. Don't do it again. #line:d7df49 +=== diff --git a/Ship.yarn b/Ship.yarn new file mode 100644 index 0000000..cbc696f --- /dev/null +++ b/Ship.yarn @@ -0,0 +1,34 @@ +title: Ship +tags: +colorID: 0 +position: 721,130 +--- +<> + Ship: Hey, friend. #line:5837f2 + Player: Hi, Ship. #line:ship9 + Player: How's space? #line:ship10 + Ship: Oh, man. #line:ship11 + <> + Ship: It's HUGE! #line:ship12 + <> +<> + <> + Ship: Hey!! #line:ship13 + <> +<> + +<> + Player: Sally said you wanted to see me? #line:ship1 + <> + Ship: She totally did!! #line:ship3 + <> + Ship: She wanted me to tell you... #line:ship4 + Ship: If you ever go off-watch without resetting the console again... #line:ship5 + <> + Ship: She'll flay you alive! #line:ship6 + <> + <> + Player: Uh. #line:ship7 + <> +<> +=== diff --git a/TestScene.tscn b/TestScene.tscn new file mode 100644 index 0000000..9e03b5f --- /dev/null +++ b/TestScene.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=1] +[ext_resource path="res://Dialogue.tscn" type="PackedScene" id=2] + +[node name="TestScene" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Dialogue" parent="." instance=ExtResource( 2 )] + +[node name="Wol" type="Node" parent="Dialogue"] +script = ExtResource( 1 ) +path = "res://dialogue.yarn" +auto_start = true +variable_storage = { +} From 0fd71a9940e24a1e349d32a0f70ec00f9bae4e0a Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Sat, 27 Nov 2021 22:05:24 +0100 Subject: [PATCH 02/19] feat: added example + tutorial (also did some fixes) --- Dialogue.gd | 2 +- ExampleDialogue/ExampleScene.gd | 24 +++ ExampleDialogue/ExampleScene.tscn | 232 +++++++++++++++++++++++ ExampleDialogue/Player.gd | 32 ++++ Sally.yarn => ExampleDialogue/Sally.yarn | 0 Ship.yarn => ExampleDialogue/Ship.yarn | 0 ExampleScene.tscn | 66 ------- README.md | 72 +++---- addons/Wol/Wol.gd | 6 +- addons/Wol/core/VirtualMachine.gd | 11 +- addons/Wol/core/compiler/lexer.gd | 2 +- addons/Wol/core/compiler/parser.gd | 7 +- 12 files changed, 343 insertions(+), 111 deletions(-) create mode 100644 ExampleDialogue/ExampleScene.gd create mode 100644 ExampleDialogue/ExampleScene.tscn create mode 100644 ExampleDialogue/Player.gd rename Sally.yarn => ExampleDialogue/Sally.yarn (100%) rename Ship.yarn => ExampleDialogue/Ship.yarn (100%) delete mode 100644 ExampleScene.tscn diff --git a/Dialogue.gd b/Dialogue.gd index 4c243eb..dacf3e4 100644 --- a/Dialogue.gd +++ b/Dialogue.gd @@ -5,7 +5,7 @@ func _ready(): $VBoxContainer/ButtonTemplate.hide() $Wol.connect('line', self, '_on_line') - $Wol.connect('option', self, '_on_option') + $Wol.connect('options', self, '_on_options') $Wol.connect('finished', self, '_on_finished') func continue_dialogue(): diff --git a/ExampleDialogue/ExampleScene.gd b/ExampleDialogue/ExampleScene.gd new file mode 100644 index 0000000..b3e64b5 --- /dev/null +++ b/ExampleDialogue/ExampleScene.gd @@ -0,0 +1,24 @@ +extends Node2D + +var current_dialogue + +func _ready(): + $Sally/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Sally, true]) + $Sally/DialogueStarter.connect('body_exited', self, '_on_player_near_dialogue', [$Sally, false]) + $Ship/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Ship, true]) + $Ship/DialogueStarter.connect('body_exited', self, '_on_player_near_dialogue', [$Ship, false]) + +func _on_player_near_dialogue(_player, node, entered): + print('body entered?', entered) + if entered: + current_dialogue = node.name + else: + current_dialogue = null + +func _process(_delta): + if Input.is_action_just_released('ui_accept') and current_dialogue and not $Dialogue/Wol.running: + print(current_dialogue) + $Dialogue/Wol.starting_node = current_dialogue + $Dialogue/Wol.path = 'res://ExampleDialogue/%s.yarn' % current_dialogue + $Dialogue/Wol.start() + print($Dialogue/Wol.variable_storage) diff --git a/ExampleDialogue/ExampleScene.tscn b/ExampleDialogue/ExampleScene.tscn new file mode 100644 index 0000000..98806f7 --- /dev/null +++ b/ExampleDialogue/ExampleScene.tscn @@ -0,0 +1,232 @@ +[gd_scene load_steps=7 format=2] + +[ext_resource path="res://Dialogue.tscn" type="PackedScene" id=1] +[ext_resource path="res://ExampleDialogue/Player.gd" type="Script" id=2] +[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=3] +[ext_resource path="res://ExampleDialogue/ExampleScene.gd" type="Script" id=4] + +[sub_resource type="RectangleShape2D" id=1] + +[sub_resource type="CircleShape2D" id=2] +radius = 62.2013 + +[node name="ExampleScene" type="Node2D"] +script = ExtResource( 4 ) + +[node name="Dialogue" parent="." instance=ExtResource( 1 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 1024.0 +margin_bottom = 600.0 + +[node name="Wol" type="Node" parent="Dialogue"] +script = ExtResource( 3 ) +variable_storage = { +} + +[node name="Player" type="KinematicBody2D" parent="."] +position = Vector2( 360, 488 ) +script = ExtResource( 2 ) + +[node name="Visuals" type="Node2D" parent="Player"] + +[node name="ColorRect" type="ColorRect" parent="Player/Visuals"] +margin_left = -21.0 +margin_top = -52.0 +margin_right = 19.0 +margin_bottom = -12.0 +color = Color( 0, 0, 0, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect2" type="ColorRect" parent="Player/Visuals"] +margin_left = -8.0 +margin_top = -47.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect4" type="ColorRect" parent="Player/Visuals"] +margin_top = -23.0 +margin_right = 8.0 +margin_bottom = -19.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect3" type="ColorRect" parent="Player/Visuals"] +margin_left = 7.0 +margin_top = -47.0 +margin_right = 15.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="LeftFoot" type="ColorRect" parent="Player"] +margin_left = -12.0 +margin_top = -14.0 +margin_right = -8.0 +margin_bottom = 5.0 +color = Color( 0, 0, 0, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="RightFoot" type="ColorRect" parent="Player"] +margin_left = 5.0 +margin_top = -14.0 +margin_right = 9.0 +margin_bottom = 5.0 +color = Color( 0, 0, 0, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] +position = Vector2( -1, -9 ) +shape = SubResource( 1 ) + +[node name="Sally" type="KinematicBody2D" parent="."] +position = Vector2( 755, 595 ) + +[node name="DialogueStarter" type="Area2D" parent="Sally"] +collision_layer = 0 +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Sally/DialogueStarter"] +shape = SubResource( 2 ) + +[node name="Visuals" type="Node2D" parent="Sally"] +position = Vector2( 1, 3 ) +scale = Vector2( -1, 1 ) + +[node name="ColorRect" type="ColorRect" parent="Sally/Visuals"] +margin_left = -21.0 +margin_top = -52.0 +margin_right = 19.0 +margin_bottom = -12.0 +color = Color( 0.729412, 0.160784, 0.160784, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect2" type="ColorRect" parent="Sally/Visuals"] +margin_left = -8.0 +margin_top = -47.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect4" type="ColorRect" parent="Sally/Visuals"] +margin_top = -23.0 +margin_right = 8.0 +margin_bottom = -19.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect3" type="ColorRect" parent="Sally/Visuals"] +margin_left = 7.0 +margin_top = -47.0 +margin_right = 15.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="LeftFoot" type="ColorRect" parent="Sally"] +margin_left = -12.0 +margin_top = -14.0 +margin_right = -8.0 +margin_bottom = 5.0 +color = Color( 0.729412, 0.160784, 0.160784, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="RightFoot" type="ColorRect" parent="Sally"] +margin_left = 5.0 +margin_top = -14.0 +margin_right = 9.0 +margin_bottom = 5.0 +color = Color( 0.729412, 0.160784, 0.160784, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Ship" type="KinematicBody2D" parent="."] +position = Vector2( 43, 595 ) + +[node name="DialogueStarter" type="Area2D" parent="Ship"] +collision_layer = 0 +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Ship/DialogueStarter"] +shape = SubResource( 2 ) + +[node name="Visuals" type="Node2D" parent="Ship"] +position = Vector2( 1, 3 ) + +[node name="ColorRect" type="ColorRect" parent="Ship/Visuals"] +margin_left = -21.0 +margin_top = -52.0 +margin_right = 19.0 +margin_bottom = -12.0 +color = Color( 0.329412, 0.231373, 1, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect2" type="ColorRect" parent="Ship/Visuals"] +margin_left = -8.0 +margin_top = -47.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect4" type="ColorRect" parent="Ship/Visuals"] +margin_top = -23.0 +margin_right = 8.0 +margin_bottom = -19.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ColorRect3" type="ColorRect" parent="Ship/Visuals"] +margin_left = 7.0 +margin_top = -47.0 +margin_right = 15.0 +margin_bottom = -28.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="LeftFoot" type="ColorRect" parent="Ship"] +margin_left = -12.0 +margin_top = -14.0 +margin_right = -8.0 +margin_bottom = 5.0 +color = Color( 0.329412, 0.231373, 1, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="RightFoot" type="ColorRect" parent="Ship"] +margin_left = 5.0 +margin_top = -14.0 +margin_right = 9.0 +margin_bottom = 5.0 +color = Color( 0.329412, 0.231373, 1, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="StaticBody2D" type="StaticBody2D" parent="."] + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"] +polygon = PoolVector2Array( -3, 427, -40, 426, -40, 648, 838, 647, 838, 419, 787, 420, 788, 600, 2, 600, 0, 420 ) diff --git a/ExampleDialogue/Player.gd b/ExampleDialogue/Player.gd new file mode 100644 index 0000000..adf6dd8 --- /dev/null +++ b/ExampleDialogue/Player.gd @@ -0,0 +1,32 @@ +extends KinematicBody2D + +var velocity = Vector2.ZERO +var time = .0 + +func _physics_process(_delta): + var gravity = Vector2.DOWN * 9.81 + + velocity += gravity + + move_and_slide(velocity, Vector2.UP) + +func _process(delta): + var direction = Input.get_vector('ui_left', 'ui_right', 'ui_select', 'ui_select') + + # Jump + if Input.is_action_just_released('ui_select') and is_on_floor(): + velocity += Vector2.UP * 9.81 * 50 + + velocity.x = direction.x * 200 + + if direction.x != 0: + time += delta + + $LeftFoot.visible = fmod(time * 4, 2) > 1 + $RightFoot.visible = not $LeftFoot.visible + else: + $LeftFoot.visible = true + $RightFoot.visible = true + + $Visuals.scale.x = -1 if velocity.x < 0 else 1 + diff --git a/Sally.yarn b/ExampleDialogue/Sally.yarn similarity index 100% rename from Sally.yarn rename to ExampleDialogue/Sally.yarn diff --git a/Ship.yarn b/ExampleDialogue/Ship.yarn similarity index 100% rename from Ship.yarn rename to ExampleDialogue/Ship.yarn diff --git a/ExampleScene.tscn b/ExampleScene.tscn deleted file mode 100644 index 492d74f..0000000 --- a/ExampleScene.tscn +++ /dev/null @@ -1,66 +0,0 @@ -[gd_scene load_steps=4 format=2] - -[ext_resource path="res://addons/Wol/Wol.gd" type="Script" id=1] -[ext_resource path="res://Dialogue.tscn" type="PackedScene" id=2] - -[sub_resource type="RectangleShape2D" id=1] - -[node name="ExampleScene" type="Node2D"] - -[node name="Dialogue" parent="." instance=ExtResource( 2 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -margin_right = 1024.0 -margin_bottom = 600.0 - -[node name="Wol" type="Node" parent="Dialogue"] -script = ExtResource( 1 ) -variable_storage = { -} - -[node name="Player" type="KinematicBody2D" parent="."] -position = Vector2( 360, 488 ) - -[node name="ColorRect" type="ColorRect" parent="Player"] -margin_left = -21.0 -margin_top = -52.0 -margin_right = 19.0 -margin_bottom = -12.0 -color = Color( 0, 0, 0, 1 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="ColorRect2" type="ColorRect" parent="Player"] -margin_left = -8.0 -margin_top = -47.0 -margin_bottom = -28.0 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="ColorRect4" type="ColorRect" parent="Player"] -margin_top = -23.0 -margin_right = 8.0 -margin_bottom = -19.0 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="ColorRect3" type="ColorRect" parent="Player"] -margin_left = 7.0 -margin_top = -47.0 -margin_right = 15.0 -margin_bottom = -28.0 -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] -position = Vector2( -1, -9 ) -shape = SubResource( 1 ) - -[node name="StaticBody2D" type="StaticBody2D" parent="."] - -[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"] -polygon = PoolVector2Array( -3, 427, -40, 426, -40, 648, 838, 647, 838, 419, 787, 420, 788, 600, 2, 600, 0, 420 ) diff --git a/README.md b/README.md index bc19944..7e3de6f 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,11 @@ Unfortunately, this option isn't available yet. Stay tuned! There are few things that need to be ironed out to be 100% feature compatible with the original YarnSpinner. - [ ] Integration with Godot's translation/localization system. -- [ ] Full support for [format functions](https://yarnspinner.dev/docs/syntax/#format-functions). + - [ ] Auto generation for `#line:` suffixes +- [ ] Support for [format functions](https://yarnspinner.dev/docs/syntax/#format-functions). +- [ ] Support for conditional options. - [ ] In-editor dialogue editor with preview. -- [ ] Fully extend the documentation of this project. - - [ ] Replicate the files needed for the Yarn Spinner tutorial. - - [x] Document the [Option](README.md#Option) object. - - [x] Write the method descriptions for the `Wol` node. - - [x] Provide helpful anchors in the documentation. +- [x] Fully extend the documentation of this project. - [x] Porting to usable signals in Godot. - [x] Providing helpful errors when failing to compile. - [x] Having a working repository with example code. @@ -280,10 +278,10 @@ It has a reference to a [Line](README.md#Line) with it's `line` property so you # Tutorial -Welcome to Yarn Spinner! In this tutorial, you’ll learn how to use Wol in a Godot project to create interactive dialogue. +Welcome to Wol! In this tutorial, you’ll learn how to use Wol in a Godot project to create interactive dialogue. -We’ll start by downloading and installing Yarn Spinner. We’ll then take a look at the core concepts that power Yarn Spinner, and write some dialogue. -After that, we’ll explore some of the more advanced features of Yarn Spinner. +We’ll start by downloading and installing Wol. We’ll then take a look at the core concepts that power Wol / Yarn, and write some dialogue. +After that, we’ll explore some of the more advanced features of Wol & Yarn. ## Introducing Yarn Spinner @@ -311,8 +309,8 @@ We’ll begin by playing the example game that comes with Wol. It’s very short 1. Create a new empty Godot project. 2. Download and install Wol. Go to the [Getting started section](#README.md#Getting-Started), and follow the directions there. -3. Open the example scene. -4. Play the game. Use the left and right arrow keys to move, and the space bar to talk to characters. +3. Open the example scene (`res://ExampleDialogue/ExampleScene.tscn`). +4. Play the game. Use the left and right arrow keys to move, and the enter key to talk to characters. We’re now ready to start looking under the hood, to see how Wol & Yarn power this game. @@ -320,7 +318,7 @@ We’re now ready to start looking under the hood, to see how Wol & Yarn power t Wol & Yarn Spinner stores its dialogue in .yarn (or .wol) files. These are plain text files, which means you can edit them in any plain text editor (Visual Studio Code is a good option, and Secret Labs offers a syntax highlighting extension to make it nice to use!) -You can also use the Wol Editor, which is a tool in the Godot editor for working with Yarn code. This editor is useful because it lets you view the structure of your dialogue in a very visual way. +You can also use the Wol Editor, which is a tool in the Godot editor for working with Yarn code. This editor is useful because it lets you view the structure of your dialogue in a very visual way. (This is not completed yet however) ### Reading Yarn @@ -328,7 +326,7 @@ In this section of the tutorial, we’re going to open the file Sally.yarn, and #### Open Sally.yarn in your editor of choice. -Yarn Spinner groups all of its dialogue into nodes. Nodes contain everything: your lines of dialogue, the choices you show to the player, and the commands that you send to the game. The Sally.yarn file contains four of them: Sally, Sally.Watch, Sally.Exit, and Sally.Sorry. The example game is set up so that when you walk up to Sally and press the spacebar, the game will start running the Sally node. +Wol & Yarn groups all of its dialogue into nodes. Nodes contain everything: your lines of dialogue, the choices you show to the player, and the commands that you send to the game. The Sally.yarn file contains four of them: Sally, Sally.Watch, Sally.Exit, and Sally.Sorry. The example game is set up so that when you walk up to Sally and press the spacebar, the game will start running the Sally node. #### Go to the Sally node. @@ -373,14 +371,16 @@ We’ll now take a closer look at each part of this code, and explain what’s g <> ``` -The first line of code in this node checks to see if Yarn Spinner has already run this node. visited is a function that this example game has defined - it isn’t built into Yarn Spinner. It returns true if the node you specify has been run before. You’ll notice that this line is wrapped in << and >> symbols. This tells Yarn Spinner that it’s control code, and not meant to be shown to the player. +The first line of code in this node checks to see if Wol has already run this node. `visited` is a function that +is built into Wol. It returns true if the node you specify has been run before. +You’ll notice that this line is wrapped in `<<` and `>>` symbols. This tells Wol that it’s control code, and not meant to be shown to the player. -If they haven’t run the Sally node yet, it means that this is the first time that we’ve spoken to Sally in this game. As a result, we run lines in which Sally and the player character meet. Otherwise, we instead run some shorter lines. +If they haven’t run the `Sally` node yet, it means that this is the first time that we’ve spoken to `Sally` in this game. As a result, we run lines in which Sally and the player character meet. Otherwise, we instead run some shorter lines. +Each line in Wol is just a run of text, which is sent directly to the game. It’s up to the game to decide how it wants to display it; in the example game, it’s shown at the top of the screen. -Each line in Yarn Spinner is just a run of text, which is sent directly to the game. It’s up to the game to decide how it wants to display it; in the example game, it’s shown at the top of the screen. +At the end of each line, you’ll see a `#line:` tag. This tag lets Wol identify lines across multiple translations, and is optional if you aren’t translating your game into other languages. Wol can automatically generate them for you (not supported yet however). -At the end of each line, you’ll see a #line: tag. This tag lets Yarn Spinner identify lines across multiple translations, and is optional if you aren’t translating your game into other languages. Yarn Spinner can automatically generate them for you. -Options +#### Options Here’s the next part of the code. @@ -396,11 +396,11 @@ Here’s the next part of the code. In the next part of the code, we do a check, and if it passes, we add an option. Options are things that the player can select; in this game, they’re things the player can say, but like lines, it’s up to the game to decide what to do with them. Options are shown to the player when the end of a node is reached. -The first couple of lines here test to see whether the player has run the node Sally.Watch. If they haven’t, then the code adds a new option. Options are wrapped with [[ and ]]. The text before the | is shown to the player, and the text after is the name of the node that will be run if the player chooses the option. Like lines, options can have line tags for localisation. +The first couple of lines here test to see whether the player has run the node `Sally.Watch`. If they haven’t, then the code adds a new option. Options are wrapped with `[[` and `]]`. The text before the `|` is shown to the player, and the text after is the name of the node that will be run if the player chooses the option. Like lines, options can have line tags for localisation. -If the player has run the Sally.Watch node before, this code won’t be run, which means that the option to run it again won’t appear. +If the player has run the `Sally.Watch` node before, this code won’t be run, which means that the option to run it again won’t appear. -The rest of this part does a similar thing as the first: it does a check, and adds another option if the check passes. In this case, it checks to see if the variable $sally_warning is true, and if the player has not yet run the Sally.Sorry node. $sally_warning is set in a different node - it’s in the node Ship, which is stored in the file Ship.yarn. +The rest of this part does a similar thing as the first: it does a check, and adds another option if the check passes. In this case, it checks to see if the variable `$sally_warning` is `true`, and if the player has not yet run the `Sally.Sorry` node. `$sally_warning` is set in a different node - it’s in the node Ship, which is stored in the file `Ship.yarn`. ```yarn [[See you later.|Sally.Exit]] #line:0facf7 @@ -408,23 +408,25 @@ The rest of this part does a similar thing as the first: it does a check, and ad The very last line of the node adds an option, which takes the player to the Sally.Exit line. Because this option isn’t inside an if statement, it’s always added. -When Yarn Spinner hits the end of the node, all of the options that have been accumulated so far will be shown to the player. Yarn Spinner will then wait for the player to make a selection, and then start running the node that they selected. +When Wol hits the end of the node, all of the options that have been accumulated so far will be shown to the player. Wol will then wait for the player to make a selection, and then start running the node that they selected. And that’s how the node works! -Writing Some Dialogue + +### Writing Some Dialogue Let’s write some dialogue! We’ll add a couple of lines to the Ship. - Open the file Ship.yarn. It contains a single node, called Ship - go to it. +> Open the file Ship.yarn. It contains a single node, called Ship - go to it. This code uses couple of features that we didn’t see in Sally: commands, and variables. -Commands -Commands are messages that Yarn Spinner sends to your game, but aren’t intended to be shown to the player. Commands let you control things in your scene, like moving the camera around, or instructing a character to move to another point. +#### Commands -Because every game is different, Yarn Spinner leaves the task of defining most commands to you. Yarn Spinner defines two built-in commands: wait, which pauses the dialogue for a certain number of seconds, and stop, which ends the dialogue immediately. +Commands are messages that Wol sends to your game, but aren’t intended to be shown to the player. Commands let you control things in your scene, like moving the camera around, or instructing a character to move to another point. -The example game defines its own command, setsprite, which is used to change the sprite that the Ship character’s face is displaying. You can see this in action in the file Ship.yarn: +Because every game is different, Wol leaves the task of defining most commands to you. Wol defines two built-in commands: wait, which pauses the dialogue for a certain number of seconds, and stop, which ends the dialogue immediately. + +The example game defines its own command, `setsprite`, which is used to change the sprite that the Ship character’s face is displaying. You can see this in action in the file `Ship.yarn`: ```yarn Player: How's space? @@ -434,12 +436,14 @@ Ship: It's HUGE! <> ``` -You can learn how to define your own custom commands in Working With Commands. -Variables + -Variables are how you store information about what the player has done in the game. We saw variables in use in the Sally node, where the variable $sally_warning was used to control whether some content was shown or not. This variable is set in here, in the Ship node - it represents whether or not the player has heard Sally’s warning about the console from the Ship. +#### Variables -Variables in Yarn Spinner start with a $, and can store text, numbers, booleans (true or false values), or null. If you try and access a variable that hasn’t been set, you’ll get the value null, which represents “no value”. +Variables are how you store information about what the player has done in the game. We saw variables in use in the `Sally` node, where the variable `$sally_warning` was used to control whether some content was shown or not. This variable is set in here, in the `Ship` node - it represents whether or not the player has heard Sally’s warning about the console from the Ship. + +Variables in Wol start with a `$`, and can store text, numbers, booleans (`true` or `false` values), or `null`. If you try and access a variable that hasn’t been set, you’ll get the value `null`, which represents “no value”. Adding Some Content #### Add some new dialogue. Add the following text to the end of the node: @@ -457,7 +461,7 @@ Ship: Bye! #### Shortcut Options -The `->` items that we just added are called shortcut options. Shortcut options let you put choices in your node without having to create new nodes, which you link to through the [[Option]] syntax. They exist in-line with the rest of your node. +The `->` items that we just added are called shortcut options. Shortcut options let you put choices in your node without having to create new nodes, which you link to through the `[[Option]]` syntax. They exist in-line with the rest of your node. To use a shortcut option, you write a `->`, followed by the text that you want to display. Then, on the next lines, indent the code a few spaces (it doesn’t matter how many, as long as you’re consistent.) The indented lines will run if the option they’re attached to is selected. Shortcut options can be nested, which means you can put a group of shortcut options inside another. You can put any kind of code inside a shortcut option’s lines. @@ -469,4 +473,4 @@ Save the file, and go back to the game. Play the game again, and talk to the Shi The example game is set up so that when you talk to Sally, the node Sally is run, and when you talk to the Ship, the node Ship is run. With this in mind, change the story so that after you get told off by Sally, she asks you to go and fix a problem with the Ship. -You can also read the Syntax Reference for Yarn Spinner. +You can also read the [Syntax Reference](https://yarnspinner.dev/docs/syntax/) for Yarn. diff --git a/addons/Wol/Wol.gd b/addons/Wol/Wol.gd index dd6f04b..8b20d44 100644 --- a/addons/Wol/Wol.gd +++ b/addons/Wol/Wol.gd @@ -24,6 +24,8 @@ export var auto_substitute = true export(Dictionary) var variable_storage = {} +var running = false + const Constants = preload('res://addons/Wol/core/Constants.gd') const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') const Library = preload('res://addons/Wol/core/Library.gd') @@ -49,7 +51,7 @@ func _ready(): func set_path(_path): path = _path - if not Engine.editor_hint and virtual_machine: + if not Engine.editor_hint and virtual_machine and not path.empty(): var compiler = Compiler.new(path) virtual_machine.program = compiler.compile() @@ -80,6 +82,7 @@ func _on_options(options): return Constants.HandlerState.PauseExecution func _on_dialogue_finished(): + running = false emit_signal('finished') func _on_node_start(node): @@ -98,6 +101,7 @@ func pause(): func start(node = starting_node): emit_signal('started') + running = true virtual_machine.set_node(node) virtual_machine.start() diff --git a/addons/Wol/core/VirtualMachine.gd b/addons/Wol/core/VirtualMachine.gd index b86b503..96066c2 100644 --- a/addons/Wol/core/VirtualMachine.gd +++ b/addons/Wol/core/VirtualMachine.gd @@ -69,8 +69,8 @@ func set_node(name): if not dialogue.variable_storage[program.filename].has(name): dialogue.variable_storage[program.filename][name] = 0 - - dialogue.variable_storage[program.filename][name] += 1 + else: + dialogue.variable_storage[program.filename][name] += 1 return true func pause(): @@ -229,8 +229,11 @@ func run_instruction(instruction): Constants.ByteCode.PushVariable: var name = instruction.operands[0].value - var value = dialogue.variable_storage[name.replace('$', '')] - state.push_value(value) + if dialogue.variable_storage.has(name.replace('$', '')): + var value = dialogue.variable_storage[name.replace('$', '')] + state.push_value(value) + else: + state.push_value(null) Constants.ByteCode.StoreVariable: var value = state.peek_value() diff --git a/addons/Wol/core/compiler/lexer.gd b/addons/Wol/core/compiler/lexer.gd index 2287b3e..53faf40 100644 --- a/addons/Wol/core/compiler/lexer.gd +++ b/addons/Wol/core/compiler/lexer.gd @@ -45,7 +45,7 @@ func createstates(): patterns[Constants.TokenType.Text] = ['.*', 'any text'] patterns[Constants.TokenType.Number] = ['\\-?[0-9]+(\\.[0-9+])?', 'any number'] - patterns[Constants.TokenType.Str] = ['\'([^\'\\\\]*(?:\\.[^\'\\\\]*)*)\'', 'any text'] + patterns[Constants.TokenType.Str] = ['\"([^\"\\\\]*(?:\\.[^\"\\\\]*)*)\"', 'any text'] patterns[Constants.TokenType.TagMarker] = ['\\#', 'a tag #'] patterns[Constants.TokenType.LeftParen] = ['\\(', 'left parenthesis ('] patterns[Constants.TokenType.RightParen] = ['\\)', 'right parenthesis )'] diff --git a/addons/Wol/core/compiler/parser.gd b/addons/Wol/core/compiler/parser.gd index 5aa9670..884f9ba 100644 --- a/addons/Wol/core/compiler/parser.gd +++ b/addons/Wol/core/compiler/parser.gd @@ -37,11 +37,10 @@ func next_symbols_are(valid_types): func expect_symbol(token_types = []): var token = tokens.pop_front() as Lexer.Token - + if token_types.size() == 0: if token.type == Constants.TokenType.EndOfInput: assert(false, 'Unexpected end of input') - return null return token for type in token_types: @@ -153,7 +152,7 @@ class FormatFunctionNode extends ParseNode: format_text="[" parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) - while !parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): + while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): if parser.next_symbol_is([Constants.TokenType.Text]): format_text += parser.expect_symbol().value @@ -849,7 +848,7 @@ class ExpressionNode extends ParseNode: #we should have a single root expression left #if more then we failed ---- NANI if eval_stack.size() != 1: - printerr('[%s] Error parsing expression (stack did not reduce correctly )' % first.name) + printerr('[%s] Error parsing expression (stack did not reduce correctly )' % first) return eval_stack.pop_back() From 4cb93851b1c049a5233840c1afc5117cd991c666 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:31:55 +0100 Subject: [PATCH 03/19] made several fixes and started work on editor --- .gitattributes | 1 + ExampleDialogue/ExampleScene.gd | 11 +- ExampleDialogue/ExampleScene.tscn | 4 + addons/Wol/core/StandardLibrary.gd | 4 + addons/Wol/core/VirtualMachine.gd | 4 +- addons/Wol/core/compiler/compiler.gd | 162 +++++++---- addons/Wol/core/compiler/lexer.gd | 30 +- addons/Wol/core/compiler/parser.gd | 340 +++++++++++++---------- addons/Wol/core/constants.gd | 1 + addons/Wol/core/program.gd | 1 - addons/Wol/editor/Editor.gd | 56 ++++ addons/Wol/editor/GraphNodeTemplate.tscn | 48 ++++ addons/Wol/editor/WolEditor.gd | 103 +++++++ addons/Wol/editor/WolEditor.tscn | 3 + addons/Wol/editor/WolGraphNode.gd | 46 +++ addons/Wol/plugin.gd | 26 ++ dialogue.yarn | 36 +-- project.godot | 5 + 18 files changed, 640 insertions(+), 241 deletions(-) create mode 100644 .gitattributes create mode 100644 addons/Wol/editor/Editor.gd create mode 100644 addons/Wol/editor/GraphNodeTemplate.tscn create mode 100644 addons/Wol/editor/WolEditor.gd create mode 100644 addons/Wol/editor/WolEditor.tscn create mode 100644 addons/Wol/editor/WolGraphNode.gd diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a569ab1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/addons/Wol/editor/WolEditor.tscn filter=lfs diff=lfs merge=lfs -text diff --git a/ExampleDialogue/ExampleScene.gd b/ExampleDialogue/ExampleScene.gd index b3e64b5..9b69dc4 100644 --- a/ExampleDialogue/ExampleScene.gd +++ b/ExampleDialogue/ExampleScene.gd @@ -8,17 +8,20 @@ func _ready(): $Ship/DialogueStarter.connect('body_entered', self, '_on_player_near_dialogue', [$Ship, true]) $Ship/DialogueStarter.connect('body_exited', self, '_on_player_near_dialogue', [$Ship, false]) + $Dialogue/Wol.connect('finished', self, '_on_finished') + func _on_player_near_dialogue(_player, node, entered): - print('body entered?', entered) if entered: current_dialogue = node.name else: current_dialogue = null +func _on_finished(): + $DialogueCooldown.start() + func _process(_delta): - if Input.is_action_just_released('ui_accept') and current_dialogue and not $Dialogue/Wol.running: - print(current_dialogue) + if Input.is_action_just_released('ui_accept') \ + and current_dialogue and not $Dialogue/Wol.running and $DialogueCooldown.time_left == 0: $Dialogue/Wol.starting_node = current_dialogue $Dialogue/Wol.path = 'res://ExampleDialogue/%s.yarn' % current_dialogue $Dialogue/Wol.start() - print($Dialogue/Wol.variable_storage) diff --git a/ExampleDialogue/ExampleScene.tscn b/ExampleDialogue/ExampleScene.tscn index 98806f7..114a5b2 100644 --- a/ExampleDialogue/ExampleScene.tscn +++ b/ExampleDialogue/ExampleScene.tscn @@ -230,3 +230,7 @@ __meta__ = { [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"] polygon = PoolVector2Array( -3, 427, -40, 426, -40, 648, 838, 647, 838, 419, 787, 420, 788, 600, 2, 600, 0, 420 ) + +[node name="DialogueCooldown" type="Timer" parent="."] +wait_time = 0.4 +one_shot = true diff --git a/addons/Wol/core/StandardLibrary.gd b/addons/Wol/core/StandardLibrary.gd index 9f5abf5..00adb96 100644 --- a/addons/Wol/core/StandardLibrary.gd +++ b/addons/Wol/core/StandardLibrary.gd @@ -79,7 +79,11 @@ func node_visit_count(node = virtual_machine.current_node.name): if node is Value: node = virtual_machine.program.strings[node.value()].text + var variable_storage = virtual_machine.dialogue.variable_storage var visited_node_count = variable_storage[virtual_machine.program.filename] + print('checking node "%s"' % node) + print(variable_storage) + return visited_node_count[node] if visited_node_count.has(node) else 0 diff --git a/addons/Wol/core/VirtualMachine.gd b/addons/Wol/core/VirtualMachine.gd index 96066c2..ad66224 100644 --- a/addons/Wol/core/VirtualMachine.gd +++ b/addons/Wol/core/VirtualMachine.gd @@ -68,7 +68,7 @@ func set_node(name): dialogue.variable_storage[program.filename] = {} if not dialogue.variable_storage[program.filename].has(name): - dialogue.variable_storage[program.filename][name] = 0 + dialogue.variable_storage[program.filename][name] = 1 else: dialogue.variable_storage[program.filename][name] += 1 return true @@ -287,7 +287,7 @@ func run_instruction(instruction): var line = option[0] var destination = option[1] choices.append(Program.Option.new(line, option_index, destination)) - + execution_state = Constants.ExecutionState.WaitingForOption options_handler.call_func(choices) diff --git a/addons/Wol/core/compiler/compiler.gd b/addons/Wol/core/compiler/compiler.gd index aafd6a7..617042d 100644 --- a/addons/Wol/core/compiler/compiler.gd +++ b/addons/Wol/core/compiler/compiler.gd @@ -1,5 +1,7 @@ extends Object +signal error(message, line_number, column) + const Constants = preload('res://addons/Wol/core/Constants.gd') const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') const Program = preload('res://addons/Wol/core/Program.gd') @@ -12,13 +14,15 @@ var filename = '' var current_node var has_implicit_string_tags = false +var soft_assert = false var string_count = 0 var string_table = {} var label_count = 0 -func _init(_filename, _source = null): +func _init(_filename, _source = null, _soft_assert = false): filename = _filename + soft_assert = _soft_assert if not _filename and _source: self.source = _source @@ -28,69 +32,119 @@ func _init(_filename, _source = null): self.source = file.get_as_text() file.close() -func compile(): - var header_sep = RegEx.new() - var header_property = RegEx.new() - header_sep.compile('---(\r\n|\r|\n)') - header_property.compile('(?.*): *(?.*)') - - assert(header_sep.search(source), 'No headers found!') - - var line_number = 0 - - var source_lines = source.split('\n', false) + var source_lines = source.split('\n') for i in range(source_lines.size()): source_lines[i] = source_lines[i].strip_edges(false, true) - var parsed_nodes = [] + source = source_lines.join('\n') + +func get_headers(offset = 0): + var header_property = RegEx.new() + var header_sep = RegEx.new() + + header_sep.compile('---(\r\n|\r|\n)') + header_property.compile('(?.*): *(?.*)') + + self.assert(header_sep.search(source), 'No headers found!') + + var title = '' + var position = Vector2.ZERO + + var source_lines = source.split('\n') + var line_number = offset while line_number < source_lines.size(): - var title = '' - var body = '' - - # Parse header - while true: - var line = source_lines[line_number] - line_number += 1 - - if not line.empty(): - var result = header_property.search(line) - if result != null: - var field = result.get_string('field') - var value = result.get_string('value') - - if field == 'title': - var regex = RegEx.new() - regex.compile(INVALID_TITLE) - assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)]) - - title = value - # TODO: Implement position, color and tags - - if line_number >= source_lines.size() or line == '---': - break - - # past header - var body_lines = [] + var line = source_lines[line_number] + line_number += 1 - while line_number < source_lines.size() and source_lines[line_number] != '===': - body_lines.append(source_lines[line_number]) - line_number += 1 + if not line.empty(): + var result = header_property.search(line) + if result != null: + var field = result.get_string('field') + var value = result.get_string('value') + + if field == 'title': + var regex = RegEx.new() + regex.compile(INVALID_TITLE) + self.assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)]) + + title = value + + if field == 'position': + var regex = RegEx.new() + regex.compile('^position:.*,.*\\d$') + self.assert(regex.search(line), 'Couldn\'t parse position property in the headers, got "%s" instead in node "%s"' % [value, title]) + + position = Vector2(int(value.split(',')[0].strip_edges()), int(value.split(',')[1].strip_edges())) + + # TODO: Implement color and tags + + if line == '---': + break + + return { + 'title': title, + 'position': position + } + +func get_body(offset = 0): + var body_lines = [] + + var source_lines = source.split('\n') + var recording = false + var line_number = offset + + while line_number < source_lines.size() and source_lines[line_number] != '===': + if recording: + body_lines.append(source_lines[line_number]) + + recording = recording or source_lines[line_number] == '---' line_number += 1 - body = PoolStringArray(body_lines).join('\n') - var lexer = Lexer.new(filename, title, body) - var tokens = lexer.tokenize() + line_number += 1 - var parser = Parser.new(title, tokens) - var parser_node = parser.parse_node() + return PoolStringArray(body_lines).join('\n') - parser_node.name = title - # parser_node.tags = title - parsed_nodes.append(parser_node) +func get_nodes(): + var nodes = [] + var line_number = 0 + var source_lines = source.split('\n') + while line_number < source_lines.size(): + var headers = get_headers(line_number) + var body = get_body(line_number) + headers.body = body + + nodes.append(headers) + + # Add +2 to the final line to skip the === from that node + line_number = Array(source_lines).find_last(body.split('\n')[-1]) + 2 while line_number < source_lines.size() and source_lines[line_number].empty(): line_number += 1 + + return nodes + +func assert(statement, message, line_number = -1, column = -1, _absolute_line_number = -1): + if not soft_assert: + assert(statement, message + ('; on line %d column %d' % [line_number, column])) + elif not statement: + emit_signal('error', message, line_number, column) + +func compile(): + var parsed_nodes = [] + for node in get_nodes(): + var lexer = Lexer.new(self, filename, node.title, node.body) + var tokens = lexer.tokenize() + + # In case of lexer error + if not tokens: + return + + var parser = Parser.new(self, node.title, tokens) + var parser_node = parser.parse_node() + + parser_node.name = node.title + parsed_nodes.append(parser_node) var program = Program.new() program.filename = filename @@ -104,7 +158,7 @@ func compile(): return program func compile_node(program, parsed_node): - assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name) + self.assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name) var node_compiled = Program.WolNode.new() @@ -205,7 +259,7 @@ func generate_statement(node, statement): Constants.StatementTypes.Line: generate_line(node, statement) _: - assert(false, 'Illegal statement type [%s]. Could not generate code.' % statement.type) + self.assert(false, statement.line_number, 'Illegal statement type [%s]. Could not generate code.' % statement.type) func generate_custom_command(node, command): # TODO: See if the first tree of this statement is being used @@ -234,7 +288,7 @@ func generate_shortcut_group(node, shortcut_group): var end = register_label('group_end') var labels = [] var option_count = 0 - + for option in shortcut_group.options: var endof_clause = '' var op_destination = register_label('option_%s' % [option_count + 1]) diff --git a/addons/Wol/core/compiler/lexer.gd b/addons/Wol/core/compiler/lexer.gd index 53faf40..b651b99 100644 --- a/addons/Wol/core/compiler/lexer.gd +++ b/addons/Wol/core/compiler/lexer.gd @@ -2,7 +2,7 @@ extends Object const Constants = preload('res://addons/Wol/core/Constants.gd') -const LINE_COMENT = '//' +const LINE_COMMENT = '//' const FORWARD_SLASH = '/' const LINE_SEPARATOR = '\n' @@ -22,6 +22,7 @@ const FORMAT_FUNCTION = 'format' var WHITESPACE = '\\s*' +var compiler var filename = '' var title = '' var text = '' @@ -33,14 +34,15 @@ var current_state var indent_stack = [] var should_track_indent = false -func _init(_filename, _title, _text): - createstates() +func _init(_compiler, _filename, _title, _text): + create_states() + compiler = _compiler filename = _filename title = _title text = _text -func createstates(): +func create_states(): var patterns = {} patterns[Constants.TokenType.Text] = ['.*', 'any text'] @@ -111,8 +113,8 @@ func createstates(): states[BASE].add_transition(Constants.TokenType.TagMarker, TAG, true) states[BASE].add_text_rule(Constants.TokenType.Text) - #TODO: FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value} - # Possible solution is to add more transitions + #FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value} + # Possible solution is to add more transitions states[TAG] = LexerState.new(patterns) states[TAG].add_transition(Constants.TokenType.Identifier, BASE) @@ -236,7 +238,11 @@ func tokenize(): lines.append('') for line in lines: - tokens += tokenize_line(line, line_number) + var line_tokens = tokenize_line(line, line_number) + if line_tokens == null: + return + + tokens.append_array(line_tokens) line_number += 1 var end_of_input = Token.new( @@ -252,12 +258,12 @@ func tokenize(): func tokenize_line(line, line_number): var token_stack = [] - var fresh_line = line.replace('\t',' ').replace('\r','') + var fresh_line = line.replace('\t', ' ').replace('\r', '') var indentation = line_indentation(line) var previous_indentation = indent_stack.front()[0] - if should_track_indent && indentation > previous_indentation: + if should_track_indent and indentation > previous_indentation: indent_stack.push_front([indentation, true]) var indent = Token.new( @@ -284,7 +290,7 @@ func tokenize_line(line, line_number): whitespace.compile(WHITESPACE) while column < fresh_line.length(): - if fresh_line.substr(column).begins_with(LINE_COMENT): + if fresh_line.substr(column).begins_with(LINE_COMMENT): break var matched = false @@ -367,12 +373,12 @@ func tokenize_line(line, line_number): line_number, column ] - assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data) + compiler.assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data, line_number, column) + return var last_whitespace = whitespace.search(line, column) if last_whitespace: column += last_whitespace.get_string().length() - token_stack.invert() diff --git a/addons/Wol/core/compiler/parser.gd b/addons/Wol/core/compiler/parser.gd index 884f9ba..b4385eb 100644 --- a/addons/Wol/core/compiler/parser.gd +++ b/addons/Wol/core/compiler/parser.gd @@ -1,13 +1,17 @@ extends Object +# warnings-disable + const Constants = preload('res://addons/Wol/core/Constants.gd') const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') const Value = preload('res://addons/Wol/core/Value.gd') -var tokens = [] +var compiler var title = '' +var tokens = [] -func _init(_title, _tokens): +func _init(_compiler, _title, _tokens): + compiler = _compiler title = _title tokens = _tokens @@ -21,13 +25,9 @@ func parse_node(): return WolNode.new('Start', null, self) func next_symbol_is(valid_types): - var type = self.tokens.front().type - for valid_type in valid_types: - if type == valid_type: - return true - return false + return tokens.front().type in valid_types -# NOTE:0 look ahead for `<<` and `else` +# NOTE: 0 look ahead for `<<` and `else` func next_symbols_are(valid_types): var temporary = [] + tokens for type in valid_types: @@ -40,7 +40,7 @@ func expect_symbol(token_types = []): if token_types.size() == 0: if token.type == Constants.TokenType.EndOfInput: - assert(false, 'Unexpected end of input') + compiler.assert(false, 'Unexpected end of input') return token for type in token_types: @@ -60,15 +60,12 @@ func expect_symbol(token_types = []): error_guess = '' var error_data = [ - token.filename, - title, - token.line_number, - token.column, PoolStringArray(token_names).join(', '), Constants.token_type_name(token.type), error_guess ] - assert(false, '[%s|%s:%d:%d]:\nExpected token "%s" but got "%s"%s' % error_data) + compiler.assert(false, 'Expected token "%s" but got "%s"%s' % error_data, token.line_number, token.column) + return token static func tab(indent_level, input, newline = true): return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n'] @@ -111,11 +108,21 @@ class WolNode extends ParseNode: var editor_node_tags = [] var statements = [] + var parser - func _init(name, parent, parser).(parent, parser): - self.name = name + func _init(_name, parent, _parser).(parent, _parser): + name = _name + parser = _parser while parser.tokens.size() > 0 \ and not parser.next_symbol_is([Constants.TokenType.Dedent, Constants.TokenType.EndOfInput]): + + parser.compiler.assert( + not parser.next_symbol_is([Constants.TokenType.Indent]), + 'Found a stray indentation!', + parser.tokens.front().line_number, + parser.tokens.front().column + ) + statements.append(Statement.new(self, parser)) func tree_string(indent_level): @@ -125,109 +132,6 @@ class WolNode extends ParseNode: return PoolStringArray(info).join('') -# TODO: Evaluate use -class Header extends ParseNode: - pass - -class InlineExpression extends ParseNode: - var expression - - func _init(parent, parser).(parent, parser): - parser.expect_symbol([Constants.TokenType.ExpressionFunctionStart]) - expression = ExpressionNode.parse(self, parser) - parser.expect_symbol([Constants.TokenType.ExpressionFunctionEnd]) - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.ExpressionFunctionStart]) - - func tree_string(_indent_level): - return "InlineExpression:" - -# Returns a format_text string as [ name "{0}" key1="value1" key2="value2" ] -class FormatFunctionNode extends ParseNode: - var format_text = '' - var expression_value - - func _init(parent:ParseNode, parser, expressionCount:int).(parent, parser): - format_text="[" - parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) - - while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): - if parser.next_symbol_is([Constants.TokenType.Text]): - format_text += parser.expect_symbol().value - - if InlineExpression.can_parse(parser): - expression_value = InlineExpression.new(self, parser) - format_text +=" \"{%d}\" " % expressionCount - parser.expect_symbol() - format_text+="]" - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.FormatFunctionStart]) - - # TODO: Make format prettier and add more information - func tree_string(_indent_level): - return "FormatFunction" - -class LineNode extends ParseNode: - var line_text = '' - # FIXME: Right now we are putting the formatfunctions and inline expressions in the same - # list but if at some point we want to stronly type our sub list we need to make a new - # parse node that can have either an InlineExpression or a FunctionFormat - # .. This is a consideration for Godot4.x - var substitutions = [] - var line_id = '' - var line_tags = [] - - # NOTE: If format function an inline functions are both present - # returns a line in the format "Some text {0} and some other {1}[format "{2}" key="value" key="value"]" - - func _init(parent, parser).(parent, parser): - while parser.next_symbol_is( - [ - Constants.TokenType.FormatFunctionStart, - Constants.TokenType.ExpressionFunctionStart, - Constants.TokenType.Text, - Constants.TokenType.TagMarker - ] - ): - - if FormatFunctionNode.can_parse(parser): - var format_function = FormatFunctionNode.new(self, parser, substitutions.size()) - if format_function.expression_value != null: - substitutions.append(format_function.expression_value) - - line_text += format_function.format_text - - elif InlineExpression.can_parse(parser): - var inline_expression = InlineExpression.new(self, parser) - line_text += '{%d}' % substitutions.size() - substitutions.append(inline_expression) - - elif parser.next_symbols_are([Constants.TokenType.TagMarker, Constants.TokenType.Identifier]): - parser.expect_symbol() - var tag_token = parser.expect_symbol([ Constants.TokenType.Identifier ]) - if tag_token.value.begins_with("line:"): - if line_id.empty(): - line_id = tag_token.value - else: - printerr("Too many line_tags @[%s:%d]" %[parser.currentNodeName, tag_token.line_number]) - return - else: - tags.append(tag_token.value) - - else: - var token = parser.expect_symbol() - if token.line_number == line_number and token.type != Constants.TokenType.BeginCommand: - line_text += token.value - else: - parser.tokens.push_front(token) - break - - - func tree_string(indent_level): - return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()]) - class Statement extends ParseNode: var Type = Constants.StatementTypes @@ -270,7 +174,7 @@ class Statement extends ParseNode: type = Type.Line else: - printerr('Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string()) + parser.compiler.assert(false, 'Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string()) var tags = [] @@ -305,6 +209,107 @@ class Statement extends ParseNode: return PoolStringArray(info).join('') +class InlineExpression extends ParseNode: + var expression + + func _init(parent, parser).(parent, parser): + parser.expect_symbol([Constants.TokenType.ExpressionFunctionStart]) + expression = ExpressionNode.parse(self, parser) + parser.expect_symbol([Constants.TokenType.ExpressionFunctionEnd]) + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.ExpressionFunctionStart]) + + func tree_string(_indent_level): + return "InlineExpression:" + +# Returns a format_text string as [ name "{0}" key1="value1" key2="value2" ] +class FormatFunctionNode extends ParseNode: + var format_text = '' + var expression_value + + func _init(parent:ParseNode, parser, expressionCount:int).(parent, parser): + format_text="[" + parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) + + # FIXME: Add exit condition in case of failure + while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): + if parser.next_symbol_is([Constants.TokenType.Text]): + format_text += parser.expect_symbol().value + + if InlineExpression.can_parse(parser): + expression_value = InlineExpression.new(self, parser) + format_text +=" \"{%d}\" " % expressionCount + parser.expect_symbol() + format_text+="]" + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.FormatFunctionStart]) + + # TODO: Make format prettier and add more information + func tree_string(_indent_level): + return "FormatFunction" + +class LineNode extends ParseNode: + var line_text = '' + # FIXME: Right now we are putting the formatfunctions and inline expressions in the same + # list but if at some point we want to strongly type our sub list we need to make a new + # parse node that can have either an InlineExpression or a FunctionFormat + # .. This is a consideration for Godot4.x + var substitutions = [] + var line_id = '' + var line_tags = [] + + # NOTE: If format function an inline functions are both present + # returns a line in the format "Some text {0} and some other {1}[format "{2}" key="value" key="value"]" + + func _init(parent, parser).(parent, parser): + while parser.next_symbol_is( + [ + Constants.TokenType.FormatFunctionStart, + Constants.TokenType.ExpressionFunctionStart, + Constants.TokenType.Text, + Constants.TokenType.TagMarker + ] + ): + + if FormatFunctionNode.can_parse(parser): + var format_function = FormatFunctionNode.new(self, parser, substitutions.size()) + if format_function.expression_value != null: + substitutions.append(format_function.expression_value) + + line_text += format_function.format_text + + elif InlineExpression.can_parse(parser): + var inline_expression = InlineExpression.new(self, parser) + line_text += '{%d}' % substitutions.size() + substitutions.append(inline_expression) + + elif parser.next_symbols_are([Constants.TokenType.TagMarker, Constants.TokenType.Identifier]): + parser.expect_symbol() + var tag_token = parser.expect_symbol([ Constants.TokenType.Identifier ]) + if tag_token.value.begins_with("line:"): + if line_id.empty(): + line_id = tag_token.value + else: + printerr("Too many line_tags @[%s:%d]" % [parser.currentNodeName, tag_token.line_number]) + return + else: + tags.append(tag_token.value) + + else: + var token = parser.expect_symbol() + if token.line_number == line_number and token.type != Constants.TokenType.BeginCommand: + line_text += token.value + else: + parser.tokens.push_front(token) + break + + + func tree_string(indent_level): + return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()]) + + class CustomCommand extends ParseNode: enum Type { @@ -322,6 +327,7 @@ class CustomCommand extends ParseNode: var command_tokens = [] command_tokens.append(parser.expect_symbol()) + # FIXME: add exit condition while not parser.next_symbol_is([Constants.TokenType.EndCommand]): command_tokens.append(parser.expect_symbol()) @@ -338,9 +344,9 @@ class CustomCommand extends ParseNode: type = Type.Expression else: - #otherwise evaluuate command + # otherwise evaluate command type = Type.ClientCommand - self.client_command = command_tokens[0].value + client_command = command_tokens[0].value func tree_string(indent_level): match type: @@ -362,9 +368,7 @@ class ShortcutOptionGroup extends ParseNode: # parse options until there is no more # expect one otherwise invalid - var index = 1 - options.append(ShortCutOption.new(index, self, parser)) - index += 1 + var index = 0 while parser.next_symbol_is([Constants.TokenType.ShortcutOption]): options.append(ShortCutOption.new(index, self, parser)) index += 1 @@ -393,7 +397,7 @@ class ShortCutOption extends ParseNode: parser.expect_symbol([Constants.TokenType.ShortcutOption]) label = parser.expect_symbol([Constants.TokenType.Text]).value - # NOTE: Parse the conditional << if $x >> when it exists + # FIXME: Parse the conditional << if $x >> when it exists var tags = [] while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \ or parser.next_symbol_is([Constants.TokenType.TagMarker]): @@ -446,6 +450,7 @@ class Block extends ParseNode: parser.expect_symbol([Constants.TokenType.Indent]) #keep reading statements until we hit a dedent + # FIXME: find exit condition while not parser.next_symbol_is([Constants.TokenType.Dedent]): #parse all statements including nested blocks statements.append(Statement.new(self, parser)) @@ -669,6 +674,8 @@ class ExpressionNode extends ParseNode: # Using Djikstra's shunting-yard algorithm to convert stream of expresions into postfix notation, # & then build a tree of expressions + + # TODO: Rework expression parsing static func parse(parent, parser): var rpn = [] var op_stack = [] @@ -693,20 +700,30 @@ class ExpressionNode extends ParseNode: var last + print(parser.tokens.slice(0, 6)) #read expression content while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types): + print(parser.tokens.front()) var next = parser.expect_symbol(valid_types) - if next.type == Constants.TokenType.Variable \ - or next.type == Constants.TokenType.Number \ - or next.type == Constants.TokenType.Str \ - or next.type == Constants.TokenType.FalseToken \ - or next.type == Constants.TokenType.TrueToken \ - or next.type == Constants.TokenType.NullToken: - - #output primitives - rpn.append(next) + if next.type in [ + Constants.TokenType.Variable, + Constants.TokenType.Number, + Constants.TokenType.Str, + Constants.TokenType.FalseToken, + Constants.TokenType.TrueToken, + Constants.TokenType.NullToken + ]: + + # Output primitives + print('adding value "%s" in expression' % next) + print(op_stack.size()) + if func_stack.size() != 0: + op_stack.append(next) + else: + rpn.append(next) elif next.type == Constants.TokenType.Identifier: + print('adding function') op_stack.push_back(next) func_stack.push_back(next) @@ -719,7 +736,7 @@ class ExpressionNode extends ParseNode: while op_stack.back().type != Constants.TokenType.LeftParen: var p = op_stack.pop_back() if p == null: - printerr('unbalanced parenthesis %s ' % next.name) + printerr('unbalanced parenthesis %s' % next.name) break rpn.append(p) @@ -732,7 +749,7 @@ class ExpressionNode extends ParseNode: #find the closest function on stack #increment parameters - func_stack.back().parameter_count+=1 + func_stack.back().parameter_count += 1 elif Operator.is_op(next.type): #this is an operator @@ -759,7 +776,7 @@ class ExpressionNode extends ParseNode: next.type = Constants.TokenType.EqualTo #operator precedence - while (ExpressionNode.is_apply_precedence(next.type, op_stack)): + while ExpressionNode.is_apply_precedence(next.type, op_stack, parser): var op = op_stack.pop_back() rpn.append(op) @@ -771,20 +788,28 @@ class ExpressionNode extends ParseNode: elif next.type == Constants.TokenType.RightParen: #leaving sub expression # resolve order of operations + var parameters = [] while op_stack.back().type != Constants.TokenType.LeftParen: - rpn.append(op_stack.pop_back()) - if op_stack.back() == null: - printerr('Unbalanced parenthasis #RightParen. Parser.ExpressionNode') + parameters.append(op_stack.pop_back()) + + parser.compiler.assert( + op_stack.back() != null, + 'Unbalanced parenthasis #RightParen. Parser.ExpressionNode' + ) + rpn.append_array(parameters) op_stack.pop_back() + # FIXME: Something is going on with parameter counting, fixed for now + # but needs a bigger rework if op_stack.back().type == Constants.TokenType.Identifier: #function call #last token == left paren this == no parameters #else #we have more than 1 param - if last.type != Constants.TokenType.LeftParen: - func_stack.back().parameter_count+=1 + # if last.type != Constants.TokenType.LeftParen: + # func_stack.back().parameter_count += 1 + func_stack.back().parameter_count = parameters.size() rpn.append(op_stack.pop_back()) func_stack.pop_back() @@ -804,6 +829,8 @@ class ExpressionNode extends ParseNode: var first = rpn.front() var eval_stack = []#ExpressionNode + print(rpn) + while rpn.size() > 0: var next = rpn.pop_front() if Operator.is_op(next.type): @@ -811,7 +838,14 @@ class ExpressionNode extends ParseNode: var info = Operator.op_info(next.type) if eval_stack.size() < info.arguments: - printerr('Error parsing : Not enough arguments for %s [ got %s expected - was %s]'%[Constants.token_type_name(next.type), eval_stack.size(), info.arguments]) + printerr( + 'Error parsing : Not enough arguments for %s [ got %s expected - was %s]' \ + % [ + Constants.token_type_name(next.type), + eval_stack.size(), + info.arguments + ] + ) var function_parameters = [] for _i in range(info.arguments): @@ -827,6 +861,7 @@ class ExpressionNode extends ParseNode: # A function call elif next.type == Constants.TokenType.Identifier: var function_name = next.value + prints(function_name, next.parameter_count) var function_parameters = [] for _i in range(next.parameter_count): @@ -845,10 +880,14 @@ class ExpressionNode extends ParseNode: eval_stack.append(expression) - #we should have a single root expression left - #if more then we failed ---- NANI - if eval_stack.size() != 1: - printerr('[%s] Error parsing expression (stack did not reduce correctly )' % first) + # NOTE: We should have a single root expression left + # if more then we failed + parser.compiler.assert( + eval_stack.size() == 1, + '[%s] Error parsing expression (stack did not reduce correctly)' % first, + first.line_number, + first.column + ) return eval_stack.pop_back() @@ -860,12 +899,13 @@ class ExpressionNode extends ParseNode: return key return string - static func is_apply_precedence(_type, operator_stack): + static func is_apply_precedence(_type, operator_stack, parser): if operator_stack.size() == 0: return false if not Operator.is_op(_type): - assert(false, 'Unable to parse expression!') + parser.compiler.assert(false, 'Unable to parse expression!') + return false var second = operator_stack.back().type diff --git a/addons/Wol/core/constants.gd b/addons/Wol/core/constants.gd index c44b85e..c700d85 100644 --- a/addons/Wol/core/constants.gd +++ b/addons/Wol/core/constants.gd @@ -211,3 +211,4 @@ static func token_name(type): if TokenType[key] == type: return key return '' + diff --git a/addons/Wol/core/program.gd b/addons/Wol/core/program.gd index fa94140..4b9d282 100644 --- a/addons/Wol/core/program.gd +++ b/addons/Wol/core/program.gd @@ -29,7 +29,6 @@ class Line: func _to_string(): return '%s:%d: "%s"' % [file_name.get_file(), line_number, text] - class Option: var line var id = -1 diff --git a/addons/Wol/editor/Editor.gd b/addons/Wol/editor/Editor.gd new file mode 100644 index 0000000..3b1a108 --- /dev/null +++ b/addons/Wol/editor/Editor.gd @@ -0,0 +1,56 @@ +tool +extends Panel + +var current_node +var current_graph_node + +func _ready(): + hide() + connect('visibility_changed', self, '_on_visibility_changed') + $Close.connect('pressed', self, 'close') + +func close(): + hide() + +func open_node(graph_node, node): + current_node = node + current_graph_node = graph_node + + var text_edit = graph_node.get_node('TextEdit') + text_edit.get_parent().remove_child(text_edit) + $Content.add_child(text_edit) + toggle_text_edit(text_edit) + + show() + + # window_title = node.title + +func toggle_text_edit(text_edit): + text_edit.anchor_left = 0 + text_edit.anchor_top = 0 + text_edit.anchor_bottom = 1 + text_edit.anchor_right = 1 + text_edit.margin_left = 0 + text_edit.margin_right = 0 + text_edit.margin_bottom = 0 + text_edit.margin_top = 0 + text_edit.mouse_filter = MOUSE_FILTER_STOP if text_edit.get_parent().name == 'Content' else MOUSE_FILTER_IGNORE + + text_edit.deselect() + + for property in [ + 'highlight_current_line', + 'show_line_numbers', + 'draw_tabs', + 'smooth_scrolling', + 'wrap_enabled', + 'minimap_draw' + ]: + text_edit.set(property, not text_edit.get(property)) + +func _on_visibility_changed(): + if not visible: + var text_edit = $Content/TextEdit + $Content.remove_child(text_edit) + current_graph_node.add_child(text_edit) + toggle_text_edit(text_edit) diff --git a/addons/Wol/editor/GraphNodeTemplate.tscn b/addons/Wol/editor/GraphNodeTemplate.tscn new file mode 100644 index 0000000..9f371a8 --- /dev/null +++ b/addons/Wol/editor/GraphNodeTemplate.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/Wol/editor/WolGraphNode.gd" type="Script" id=1] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_left = 4.0 +content_margin_right = 4.0 +content_margin_top = 26.0 +content_margin_bottom = 4.0 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[node name="GraphNodeTemplate" type="GraphNode"] +margin_left = 500.0 +margin_top = 1068.0 +margin_right = 937.0 +margin_bottom = 1459.0 +mouse_filter = 1 +custom_styles/frame = SubResource( 1 ) +title = "Hello world" +offset = Vector2( 500, 500 ) +resizable = true +slot/0/left_enabled = false +slot/0/left_type = 0 +slot/0/left_color = Color( 1, 1, 1, 1 ) +slot/0/right_enabled = false +slot/0/right_type = 0 +slot/0/right_color = Color( 1, 1, 1, 1 ) +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TextEdit" type="TextEdit" parent="."] +margin_left = 4.0 +margin_top = 26.0 +margin_right = 433.0 +margin_bottom = 387.0 +mouse_filter = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +syntax_highlighting = true +fold_gutter = true +context_menu_enabled = false +virtual_keyboard_enabled = false +wrap_enabled = true diff --git a/addons/Wol/editor/WolEditor.gd b/addons/Wol/editor/WolEditor.gd new file mode 100644 index 0000000..1677b7c --- /dev/null +++ b/addons/Wol/editor/WolEditor.gd @@ -0,0 +1,103 @@ +tool +extends Control + +const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') +onready var GraphNodeTemplate = $GraphNodeTemplate + +var path +var compiler + +func _ready(): + for menu_button in [$Menu/File]: + menu_button.get_popup().connect('index_pressed', self, '_on_menu_pressed', [menu_button.get_popup()]) + + # TODO: Conditionally load in theme based on Editor or standalone + + path = 'res://dialogue.yarn' + build_nodes() + +func build_nodes(): + compiler = Compiler.new(path) + + for node in compiler.get_nodes(): + var graph_node = GraphNodeTemplate.duplicate() + $GraphEdit.add_child(graph_node) + graph_node.node = node + graph_node.show() + graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node, node]) + +func serialize_to_file(): + var buffer = [] + for graph_node in $GraphEdit.get_children(): + if not graph_node is GraphNode: + continue + + var node = graph_node.node + buffer.append('title: %s' % node.title) + buffer.append('tags: ') + buffer.append('colorID: ') + buffer.append('position: %d, %d' % [node.position.x, node.position.y]) + buffer.append('---') + buffer.append(node.body) + buffer.append('===') + + return PoolStringArray(buffer).join('\n') + +func save_as(file_path = null): + if not file_path: + $FileDialog.mode = $FileDialog.MODE_SAVE_FILE + # TODO: Set up path based on context (Godot editor, standalone or web) + $FileDialog.popup_centered() + file_path = yield($FileDialog, 'file_selected') + + if not file_path: + return + + var file = File.new() + file.open(file_path, File.WRITE) + file.store_string(serialize_to_file()) + file.close() + print('saved file!') + +func open(): + $FileDialog.mode = $FileDialog.MODE_OPEN_FILE + # TODO: Set up path based on context (Godot editor, standalone or web) + $FileDialog.popup_centered() + path = yield($FileDialog, 'file_selected') + if not path: + return + + for node in $GraphEdit.get_children(): + if node is GraphNode: + $GraphEdit.remove_child(node) + node.queue_free() + + yield(get_tree(), 'idle_frame') + build_nodes() + +func new(): + # TODO: add dialog for maybe saving existing file + + for node in $GraphEdit.get_children(): + if node is GraphNode: + $GraphEdit.remove_child(node) + node.queue_free() + + path = null + +func _on_menu_pressed(index, node): + match(node.get_item_text(index)): + 'New': + new() + 'Save': + save_as(path) + 'Save as...': + save_as() + 'Open': + open() + +func _on_graph_node_input(event, graph_node, node): + if event is InputEventMouseButton \ + and event.doubleclick and event.button_index == BUTTON_LEFT: + $HBoxContainer/Editor.open_node(graph_node, node) + accept_event() diff --git a/addons/Wol/editor/WolEditor.tscn b/addons/Wol/editor/WolEditor.tscn new file mode 100644 index 0000000..6fb5aff --- /dev/null +++ b/addons/Wol/editor/WolEditor.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2c2ef9c83d968c8cc99aa62da40477e6f3660aa71f8621e360818cc0307bf0b +size 604323 diff --git a/addons/Wol/editor/WolGraphNode.gd b/addons/Wol/editor/WolGraphNode.gd new file mode 100644 index 0000000..dc584cf --- /dev/null +++ b/addons/Wol/editor/WolGraphNode.gd @@ -0,0 +1,46 @@ +tool +extends GraphNode + +const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') + +var node setget set_node + +onready var text_edit = $TextEdit + +func _ready(): + connect('offset_changed', self, '_on_offset_changed') + text_edit.connect('text_changed', self, '_on_text_changed') + $TextDebounce.connect('timeout', self, '_on_debounce') + +func _on_text_changed(): + $TextDebounce.start(.3) + +func _on_debounce(): + text_edit.get_node('ErrorGutter').hide() + node.body = text_edit.text + compile() + +func _on_offset_changed(): + node.position = offset + +func _on_error(message, _line_number, _column): + var error_gutter = text_edit.get_node('ErrorGutter') + error_gutter.show() + error_gutter.text = message + + # TODO: Highlight line based on line number and column + +func set_node(_node): + node = _node + title = node.title + text_edit.text = node.body + text_edit.clear_undo_history() + offset = node.position + + compile() + +func compile(): + var text = '---\n%s\n===' % text_edit.text + var compiler = Compiler.new(null, text, true) + compiler.connect('error', self, '_on_error') + compiler.compile() diff --git a/addons/Wol/plugin.gd b/addons/Wol/plugin.gd index fb9984d..ed5a6cb 100644 --- a/addons/Wol/plugin.gd +++ b/addons/Wol/plugin.gd @@ -1,6 +1,10 @@ tool extends EditorPlugin +const WolEditor = preload('res://addons/Wol/editor/WolEditor.tscn') + +var wol_editor_instance + func _enter_tree(): add_custom_type( 'Wol', @@ -9,5 +13,27 @@ func _enter_tree(): load('res://addons/Wol/icon-white.svg') ) + wol_editor_instance = WolEditor.instance() + get_editor_interface().get_editor_viewport().add_child(wol_editor_instance) + + make_visible(false) + +func make_visible(visible): + if wol_editor_instance: + wol_editor_instance.visible = visible + func _exit_tree(): remove_custom_type('Wol') + + if wol_editor_instance: + wol_editor_instance.queue_free() + +func has_main_screen(): + return true + +func get_plugin_name(): + return 'Wol' + +func get_plugin_icon(): + print(get_editor_interface().get_base_control().get_icon('Node', 'EditorIcons')) + return get_editor_interface().get_base_control().get_icon('Node', 'EditorIcons') diff --git a/dialogue.yarn b/dialogue.yarn index d34b431..4faa15e 100644 --- a/dialogue.yarn +++ b/dialogue.yarn @@ -1,54 +1,54 @@ title: Start tags: -colorID: 0 +colorID: position: 0, 0 --- <> <> // remove "to" to trigger error -<> +<> <> -// Implement inline expressions +// Implement inline expressions <> -Narrator: You, {$direction} way! +Narrator: You, {$direction} way! <> Narrator: Do you know you've been here {visit_count()} times? You: Did you know one + one equals {$one + $one}? Narrator: You wanna go somewhere? - -> Go to the store - [[TheStore]] -// -> Wait, how many times have I been to the store? -// Narrator: You've been to the store {visit_count('TheStore')} times. -// [[Start]] +-> Go to the store + [[TheStore]] +-> How much did I visit the store? + Narrator: You've been to the store { visit_count("TheStore") } times. + [[Start]] -> Lets stay here and talk [[Talk]] === title: TheStore tags: -colorID: 0 -position: 0, 200 +colorID: +position: 400, 200 --- Guy: Hey what's up I need your help can you come here? You: Well I can't I'm buying clothes. -All right well hurry up and come over here. +Guy: All right well hurry up and come over here. You: I can't find them. Guy: What do you mean you can't find them? You: I can't find them there's only soup. -Guy: What do you mean there's only soup?! +Guy: What do you mean there's only soup?! You: It means there's only soup. Guy: WELL THEN GET OUT OF THE SOUP ISLE!! You: Alright you dont have to shout at me! You: There's more soup. -Guy: What do you mean there's more soup? +Guy: What do you mean there's more soup? You: There's just more soup. Guy: Then go to the next aisle! -You: There's still soup! +You: There's still soup! Guy: Where are you right now?! You: I'm at soup! -Guy: What do you mean you're at soup?! +Guy: What do you mean you're at soup?! You: I mean I'm at soup. Guy: WHAT STORE ARE YOU IN?! You: IM AT THE SOUP STORE!! @@ -58,8 +58,8 @@ You: FUCK YOU! === title: Talk tags: -colorID: 0 -position: 0, 400 +colorID: +position: 800, 400 --- Narrator: So how are you really? You: I'm good! diff --git a/project.godot b/project.godot index a351bd7..e6a9e01 100644 --- a/project.godot +++ b/project.godot @@ -24,6 +24,11 @@ gdscript/warnings/return_value_discarded=false enabled=PoolStringArray( "res://addons/Wol/plugin.cfg" ) +[network] + +limits/debugger_stdout/max_chars_per_second=8096 +limits/debugger_stdout/max_messages_per_frame=200 + [physics] common/enable_pause_aware_picking=true From 58b9fbb11177b29e01feff22adf4446649ff5018 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:33:30 +0100 Subject: [PATCH 04/19] changed a small string in the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e3de6f..53bc245 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ We’ll begin by playing the example game that comes with Wol. It’s very short We’re now ready to start looking under the hood, to see how Wol & Yarn power this game. -### The Yarn Editor +### The Wol Editor Wol & Yarn Spinner stores its dialogue in .yarn (or .wol) files. These are plain text files, which means you can edit them in any plain text editor (Visual Studio Code is a good option, and Secret Labs offers a syntax highlighting extension to make it nice to use!) From 0561a41c0d9a0c4c945d20d1d883004f97288f4e Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:35:03 +0100 Subject: [PATCH 05/19] replaced another yarn with wol --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 53bc245..ed0cdb1 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ Welcome to Wol! In this tutorial, you’ll learn how to use Wol in a Godot proje We’ll start by downloading and installing Wol. We’ll then take a look at the core concepts that power Wol / Yarn, and write some dialogue. After that, we’ll explore some of the more advanced features of Wol & Yarn. -## Introducing Yarn Spinner +## Introducing Wol Wol & Yarn (Spinner) are tools for writing interactive dialogue in games - that is, conversations that the player can have with characters in the game. Yarn Spinner does this by letting you write your dialogue in a programming language called Yarn. From 0be6309941667b6d31b911f5fe57a573ba84d75e Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:35:35 +0100 Subject: [PATCH 06/19] switched documentation with tutorial --- README.md | 391 +++++++++++++++++++++++++++--------------------------- 1 file changed, 196 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index ed0cdb1..a4419fd 100644 --- a/README.md +++ b/README.md @@ -81,201 +81,6 @@ Wol & Yarn Spinner needs your help to be as awesome as it can be! You don't have * Join Secret Lab's discussion on Slack by joining the [narrative game development](http://lab.to/narrativegamedev) channel. * Follow [Bram Dingelstad](https://twitter.com/bram_dingelstad) & [Yarn Spinner](http://twitter.com/YarnSpinnerTool). -# Documentation - -## `Wol` -_Inherits from [Node](https://docs.godotengine.org/en/stable/classes/class_node.html)_ - -Node for all interaction with Wol. - -### Description -Godot's Nodes as building blocks work really well. That's why this plugin gives you access to a simple node that does all the heavy lifting for you. -It has several properties that you can change either in-editor or using GDScript (or any other compatible language) and signals you can use to listen to events coming from your dialogue. - -### Properties -| Type | Property | Default value | -|------------------------------------------------------------------------------------------------|-------------------|---------------| -| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | path | `''` | -| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | starting_node | `'Start'` | -| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | auto_start | `false` | -| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | auto_show_options | `false` | -| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | auto_substitute | `true` | -| [Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) | variable_storage | `{}` | - -### Methods -| Return value | Method name | -|----------------------|----------------------------------------------------------------------------------------------------------------------------| -| void | start ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) starting_node = 'Start' ) | -| void | pause ( ) | -| void | resume ( ) | -| void | select_option ( [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) id ) | - -### Signals - -* started ( ) - - Emitted when the dialogue is started. - -* finished ( ) - - Emitted when the dialogue is came to a stop, either through running out of dialogue or by using the `<>` command. - -* node_started ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) node ) - - Emitted when a dialogue node is started. Has the node name as a parameter so you can see which node was started. - -* node_finished ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) node ) - - Emitted when a dialogue node is started. Has the node name as a parameter so you can see which node was started. - -* line ( [Line](README.md#Line) line ) - - Emitted when a [Line](README.md#Line) is emitted from the dialogue. `line` holds relevant information. - -* options ( [`Array`](https://docs.godotengine.org/en/stable/classes/class_array.html) options ) - - Emitted when the dialogue runs into a set of options. Is emitted with an [`Array`](https://docs.godotengine.org/en/stable/classes/class_array.html) of [Option](README.md#Option)s. - -* command ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) command ) - - Emitted when the dialogue executes a command. Use this signal to provide interactivity with your game world. - -### Property Descriptions -* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) path - - |Default|`''`| - |-------|----| - - The path to your `.yarn` or `.wol` file. Must be a valid `Yarn` file otherwise the compiler will throw an error. - -* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) starting_node - - |Default|`'Start'`| - |-------|---------| - - The node that is the starting point of the dialogue. Will automatically be the default for the `start()` function as well. The string should be a valid name for a Yarn node and be available in the file or an error will be thrown. You can always start from a different node by calling `start('OtherStartingNode')` for instance. - -* [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) auto_start - - |Default|`false`| - |-------|-------| - - If enabled, will automatically start the dialogue using the `starting_node` as the entrypoint. - -* [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) auto_show_options - - |Default|`false`| - |-------|-------| - - If enabled, will automatically show you options when they're available, rather than waiting for the player to resume to the line that has options. - -* [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) auto_substitude - - |Default|`false`| - |-------|-------| - - If enabled, will automatically substitute format functions and inline expressions for you. It's recommended to leave enabled, but if you want to manually do this for whatever reason, you can turn it off. - -* [Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) variable_storage - - |Default|`{}`| - |-------|----| - - A [Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) that holds all of the variables of your of your dialogue. - All of the entries of this dictionary are accesible in your dialogue with a `$` prefix. (e.g `a_variable` would be `$a_variable`). - If you set a variable from within your dialogue, this dictionary will also be updated. - - In the future there'll be a signal added for when the `variable_storage` is updated. - -### Method Descriptions - -* start ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) starting_node = 'Start') - - Starts the dialogue at the `starting_node` (defaults to the value of `self.starting_node` which is `Start`) - When the dialogue comes to a full stop through reaching the end or reaching a `<` command, you need to explicitly call this function instead of `resume ( )`. - -* pause ( ) - - Pauses the dialogue until `resume ( )` is called. - -* resume ( ) - - Resumes the dialogue. Won't work when the dialogue comes to a full stop by reaching the end or reaching a `<>` command. You need to call `start ( starting_node )` instead. - -* select_option ( [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) id ) - - When getting an option from the `options` signal, use this function to let Wol node which option you want to select. - Use `Option.id` for the `id` parameter. - -## `Line` -_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_ - -An object holding all information related to a line in your dialogue. - -### Description -The [Line](README.md#Line) object is _the_ object that you're gonna be interacting with the most. This object holds all of the information of the actual lines of dialogue. The most important property is `text`, but it has some additional properties you can make use of for debugging or holding of metadata (no support for that yet however). - -### Properties -| Type | Property | -|-------------------------------------------------------------------------------------------|---------------| -| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | text | -| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | node_name | -| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | file_name | -| [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) | substitutions | -| [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) | meta | - -### Property Descriptions -* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) text - - A line of dialogue that's been processed by Wol. You can use this to set a [Label](https://docs.godotengine.org/en/stable/classes/class_label.html) in a text bubble above your character, add to a [RichTextLabel](https://docs.godotengine.org/en/stable/classes/class_richtextlabel.html#class-richtextlabel) for more dynamic stuff (Wol fully supports bbcode). Look at this repository's `Dialogue.tscn` and `Dialogue.gd` for some inspiration ;) - -* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) node_name - - The name of the dialogue node this piece of dialogue came from. - -* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) file_name - - The filename of the file where this piece of dialogue came from. - -* [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) substitutions - - An [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) of [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string)s that the result of Wol proccessing statements. Use this array if you disabled `auto_substitutions` and want to manually substitute your dialogue. - -* [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) meta - - Currently unimplemented. - -## `Option` -_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_ - -An object holding information of an option in your dialogue. - -### Description -The [Option](README.md#Option) object is anoter object that you're gonna be interacting with a lot. This object holds the information of a choice in your dialogue. -It has a reference to a [Line](README.md#Line) with it's `line` property so you can show the appropriate text to your player! - -### Properties -| Type | Property | -|-------------------------------------------------------------------------------------------|---------------| -| [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) | id | -| [Line](README.md#Line) | line | -| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | destination | - -### Property Descriptions - -* [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) id - - A unique identifier that you can use to communicate your option choice with `select_option ( id )`. - -* [Line](README.md#Line) line - - A line of dialogue that's been processed by Wol. See [Line](README.md#Line) for more details. - -* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) destination - - The node that you will jump to when this option is selected. (Only relevant for jump questions, not inline ones). - - # Tutorial Welcome to Wol! In this tutorial, you’ll learn how to use Wol in a Godot project to create interactive dialogue. @@ -474,3 +279,199 @@ Save the file, and go back to the game. Play the game again, and talk to the Shi The example game is set up so that when you talk to Sally, the node Sally is run, and when you talk to the Ship, the node Ship is run. With this in mind, change the story so that after you get told off by Sally, she asks you to go and fix a problem with the Ship. You can also read the [Syntax Reference](https://yarnspinner.dev/docs/syntax/) for Yarn. + +# Documentation + +## `Wol` +_Inherits from [Node](https://docs.godotengine.org/en/stable/classes/class_node.html)_ + +Node for all interaction with Wol. + +### Description +Godot's Nodes as building blocks work really well. That's why this plugin gives you access to a simple node that does all the heavy lifting for you. +It has several properties that you can change either in-editor or using GDScript (or any other compatible language) and signals you can use to listen to events coming from your dialogue. + +### Properties +| Type | Property | Default value | +|------------------------------------------------------------------------------------------------|-------------------|---------------| +| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | path | `''` | +| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | starting_node | `'Start'` | +| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | auto_start | `false` | +| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | auto_show_options | `false` | +| [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) | auto_substitute | `true` | +| [Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) | variable_storage | `{}` | + +### Methods +| Return value | Method name | +|----------------------|----------------------------------------------------------------------------------------------------------------------------| +| void | start ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) starting_node = 'Start' ) | +| void | pause ( ) | +| void | resume ( ) | +| void | select_option ( [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) id ) | + +### Signals + +* started ( ) + + Emitted when the dialogue is started. + +* finished ( ) + + Emitted when the dialogue is came to a stop, either through running out of dialogue or by using the `<>` command. + +* node_started ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) node ) + + Emitted when a dialogue node is started. Has the node name as a parameter so you can see which node was started. + +* node_finished ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) node ) + + Emitted when a dialogue node is started. Has the node name as a parameter so you can see which node was started. + +* line ( [Line](README.md#Line) line ) + + Emitted when a [Line](README.md#Line) is emitted from the dialogue. `line` holds relevant information. + +* options ( [`Array`](https://docs.godotengine.org/en/stable/classes/class_array.html) options ) + + Emitted when the dialogue runs into a set of options. Is emitted with an [`Array`](https://docs.godotengine.org/en/stable/classes/class_array.html) of [Option](README.md#Option)s. + +* command ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) command ) + + Emitted when the dialogue executes a command. Use this signal to provide interactivity with your game world. + +### Property Descriptions +* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) path + + |Default|`''`| + |-------|----| + + The path to your `.yarn` or `.wol` file. Must be a valid `Yarn` file otherwise the compiler will throw an error. + +* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) starting_node + + |Default|`'Start'`| + |-------|---------| + + The node that is the starting point of the dialogue. Will automatically be the default for the `start()` function as well. The string should be a valid name for a Yarn node and be available in the file or an error will be thrown. You can always start from a different node by calling `start('OtherStartingNode')` for instance. + +* [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) auto_start + + |Default|`false`| + |-------|-------| + + If enabled, will automatically start the dialogue using the `starting_node` as the entrypoint. + +* [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) auto_show_options + + |Default|`false`| + |-------|-------| + + If enabled, will automatically show you options when they're available, rather than waiting for the player to resume to the line that has options. + +* [bool](https://docs.godotengine.org/en/stable/classes/class_bool.html) auto_substitude + + |Default|`false`| + |-------|-------| + + If enabled, will automatically substitute format functions and inline expressions for you. It's recommended to leave enabled, but if you want to manually do this for whatever reason, you can turn it off. + +* [Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) variable_storage + + |Default|`{}`| + |-------|----| + + A [Dictionary](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) that holds all of the variables of your of your dialogue. + All of the entries of this dictionary are accesible in your dialogue with a `$` prefix. (e.g `a_variable` would be `$a_variable`). + If you set a variable from within your dialogue, this dictionary will also be updated. + + In the future there'll be a signal added for when the `variable_storage` is updated. + +### Method Descriptions + +* start ( [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) starting_node = 'Start') + + Starts the dialogue at the `starting_node` (defaults to the value of `self.starting_node` which is `Start`) + When the dialogue comes to a full stop through reaching the end or reaching a `<` command, you need to explicitly call this function instead of `resume ( )`. + +* pause ( ) + + Pauses the dialogue until `resume ( )` is called. + +* resume ( ) + + Resumes the dialogue. Won't work when the dialogue comes to a full stop by reaching the end or reaching a `<>` command. You need to call `start ( starting_node )` instead. + +* select_option ( [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) id ) + + When getting an option from the `options` signal, use this function to let Wol node which option you want to select. + Use `Option.id` for the `id` parameter. + +## `Line` +_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_ + +An object holding all information related to a line in your dialogue. + +### Description +The [Line](README.md#Line) object is _the_ object that you're gonna be interacting with the most. This object holds all of the information of the actual lines of dialogue. The most important property is `text`, but it has some additional properties you can make use of for debugging or holding of metadata (no support for that yet however). + +### Properties +| Type | Property | +|-------------------------------------------------------------------------------------------|---------------| +| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | text | +| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | node_name | +| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | file_name | +| [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) | substitutions | +| [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) | meta | + +### Property Descriptions +* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) text + + A line of dialogue that's been processed by Wol. You can use this to set a [Label](https://docs.godotengine.org/en/stable/classes/class_label.html) in a text bubble above your character, add to a [RichTextLabel](https://docs.godotengine.org/en/stable/classes/class_richtextlabel.html#class-richtextlabel) for more dynamic stuff (Wol fully supports bbcode). Look at this repository's `Dialogue.tscn` and `Dialogue.gd` for some inspiration ;) + +* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) node_name + + The name of the dialogue node this piece of dialogue came from. + +* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) file_name + + The filename of the file where this piece of dialogue came from. + +* [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) substitutions + + An [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) of [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string)s that the result of Wol proccessing statements. Use this array if you disabled `auto_substitutions` and want to manually substitute your dialogue. + +* [Array](https://docs.godotengine.org/en/stable/classes/class_array.html) meta + + Currently unimplemented. + +## `Option` +_Inherits from [Object](https://docs.godotengine.org/en/stable/classes/class_object.html)_ + +An object holding information of an option in your dialogue. + +### Description +The [Option](README.md#Option) object is anoter object that you're gonna be interacting with a lot. This object holds the information of a choice in your dialogue. +It has a reference to a [Line](README.md#Line) with it's `line` property so you can show the appropriate text to your player! + +### Properties +| Type | Property | +|-------------------------------------------------------------------------------------------|---------------| +| [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) | id | +| [Line](README.md#Line) | line | +| [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) | destination | + +### Property Descriptions + +* [int](https://docs.godotengine.org/en/stable/classes/class_int.html#class-int) id + + A unique identifier that you can use to communicate your option choice with `select_option ( id )`. + +* [Line](README.md#Line) line + + A line of dialogue that's been processed by Wol. See [Line](README.md#Line) for more details. + +* [String](https://docs.godotengine.org/en/lastest/classes/class_string.html#class-string) destination + + The node that you will jump to when this option is selected. (Only relevant for jump questions, not inline ones). + + From 4251e63cc9ebcdde0e3b9e9e44f04275cf80cd85 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:42:27 +0100 Subject: [PATCH 07/19] capitalised some files --- TestScene.tscn | 2 +- addons/Wol/core/Constants.gd | 214 ++++++ addons/Wol/core/Library.gd | 25 + addons/Wol/core/Program.gd | 146 ++++ addons/Wol/core/Value.gd | 137 ++++ addons/Wol/core/compiler/Compiler.gd | 461 +++++++++++ addons/Wol/core/compiler/Lexer.gd | 485 ++++++++++++ addons/Wol/core/compiler/Parser.gd | 1065 ++++++++++++++++++++++++++ dialogue.yarn => dialogue.wol | 56 +- project.godot | 2 +- 10 files changed, 2563 insertions(+), 30 deletions(-) create mode 100644 addons/Wol/core/Constants.gd create mode 100644 addons/Wol/core/Library.gd create mode 100644 addons/Wol/core/Program.gd create mode 100644 addons/Wol/core/Value.gd create mode 100644 addons/Wol/core/compiler/Compiler.gd create mode 100644 addons/Wol/core/compiler/Lexer.gd create mode 100644 addons/Wol/core/compiler/Parser.gd rename dialogue.yarn => dialogue.wol (99%) diff --git a/TestScene.tscn b/TestScene.tscn index 9e03b5f..f32bb44 100644 --- a/TestScene.tscn +++ b/TestScene.tscn @@ -14,7 +14,7 @@ __meta__ = { [node name="Wol" type="Node" parent="Dialogue"] script = ExtResource( 1 ) -path = "res://dialogue.yarn" +path = "res://dialogue.wol" auto_start = true variable_storage = { } diff --git a/addons/Wol/core/Constants.gd b/addons/Wol/core/Constants.gd new file mode 100644 index 0000000..c700d85 --- /dev/null +++ b/addons/Wol/core/Constants.gd @@ -0,0 +1,214 @@ +extends Object + +enum ExecutionState { + Stopped, + Running, + WaitingForOption, + Suspended +} + +enum HandlerState { + PauseExecution, + ContinueExecution +} + +#Compile Status return +enum CompileStatus { + Succeeded, SucceededUntaggedStrings, +} + +enum ByteCode { + # opA = string: label name + Label, + # opA = string: label name + JumpTo, + # peek string from stack and jump to that label + Jump, + # opA = int: string number + RunLine, + # opA = string: command text + RunCommand, + # opA = int: string number for option to add + AddOption, + # present the current list of options, then clear the list; most recently + # selected option will be on the top of the stack + ShowOptions, + # opA = int: string number in table; push string to stack + PushString, + # opA = float: number to push to stack + PushNumber, + # opA = int (0 or 1): bool to push to stack + PushBool, + # pushes a null value onto the stack + PushNull, + # opA = string: label name if top of stack is not null, zero or false, jumps + # to that label + JumpIfFalse, + # discard top of stack + Pop, + # opA = string; looks up function, pops as many arguments as needed, result is + # pushed to stack + CallFunc, + # opA = name of variable to get value of and push to stack + PushVariable, + # opA = name of variable to store top of stack in + StoreVariable, + # stops execution + Stop, + # run the node whose name is at the top of the stack + RunNode +} + +enum TokenType { + + #0 Special tokens + Whitespace, Indent, Dedent, EndOfLine, EndOfInput, + + #5 Numbers. Everybody loves a number + Number, + + #6 Strings. Everybody also loves a string + Str, + + #7 '#' + TagMarker, + + #8 Command syntax ('<>') + BeginCommand, EndCommand, + + ExpressionFunctionStart, # { + ExpressionFunctionEnd, # } + + FormatFunctionStart, # [ + FormatFunctionEnd, # ] + + #10 Variables ('$foo') + Variable, + + #11 Shortcut syntax ('->') + ShortcutOption, + + #12 Option syntax ('[[Let's go here|Destination]]') + OptionStart, # [[ + OptionDelimit, # | + OptionEnd, # ]] + + #15 Command types (specially recognised command word) + IfToken, ElseIf, ElseToken, EndIf, Set, + + #20 Boolean values + TrueToken, FalseToken, + + #22 The null value + NullToken, + + #23 Parentheses + LeftParen, RightParen, + + #25 Parameter delimiters + Comma, + + #26 Operators + EqualTo, # ==, eq, is + GreaterThan, # >, gt + GreaterThanOrEqualTo, # >=, gte + LessThan, # <, lt + LessThanOrEqualTo, # <=, lte + NotEqualTo, # !=, neq + + #32 Logical operators + Or, # ||, or + And, # &&, and + Xor, # ^, xor + Not, # !, not + + # this guy's special because '=' can mean either 'equal to' + #36 or 'becomes' depending on context + EqualToOrAssign, # =, to + + #37 + UnaryMinus, # -; this is differentiated from Minus + # when parsing expressions + + #38 + Add, # + + Minus, # - + Multiply, # * + Divide, # / + Modulo, # % + + #43 + AddAssign, # += + MinusAssign, # -= + MultiplyAssign, # *= + DivideAssign, # /= + + Comment, # a run of text that we ignore + + Identifier, # a single word (used for functions) + + Text # a run of text until we hit other syntax +} + +enum ExpressionType { + Value, + FunctionCall +} + +enum StatementTypes { + CustomCommand, + ShortcutOptionGroup, + Block, + IfStatement, + OptionStatement, + AssignmentStatement, + Line, + InlineExpression +} + +enum ValueType { + Number, + Str, + Boolean, + Variable, + Nullean +} + +static func token_type_name(value): + for key in TokenType.keys(): + if TokenType[key] == value: + return key + return 'NOTVALID' + +static func merge_dir(target, patch): + for key in patch: + target[key] = patch[key] + +static func bytecode_name(bytecode): + return [ + 'Label', + 'JumpTo', + 'Jump', + 'RunLine', + 'RunCommand', + 'AddOption', + 'ShowOptions', + 'PushString', + 'PushNumber', + 'PushBool', + 'PushNull', + 'JumpIfFalse', + 'Pop', + 'CallFunc', + 'PushVariable', + 'StoreVariable', + 'Stop', + 'RunNode' + ][bytecode] + +static func token_name(type): + for key in TokenType.keys(): + if TokenType[key] == type: + return key + return '' + diff --git a/addons/Wol/core/Library.gd b/addons/Wol/core/Library.gd new file mode 100644 index 0000000..a0ff7c5 --- /dev/null +++ b/addons/Wol/core/Library.gd @@ -0,0 +1,25 @@ +extends Object + +const FunctionInfo = preload('res://addons/Wol/core/FunctionInfo.gd') +const Constants = preload('res://addons/Wol/core/Constants.gd') + +var functions = {} +var virtual_machine + +func get_function(name): + if functions.has(name): + return functions[name] + else : + printerr('Invalid Function: %s'% name) + return + +func import_library(other): + Constants.merge_dir(functions, other.functions) + other.virtual_machine = virtual_machine + +func register_function(name, parameter_count, function, returns_value): + var functionInfo = FunctionInfo.new(name, parameter_count, function, returns_value) + functions[name] = functionInfo + +func deregister_function(name): + functions.erase(name) diff --git a/addons/Wol/core/Program.gd b/addons/Wol/core/Program.gd new file mode 100644 index 0000000..4b9d282 --- /dev/null +++ b/addons/Wol/core/Program.gd @@ -0,0 +1,146 @@ +extends Object + +const Constants = preload('res://addons/Wol/core/Constants.gd') + +var name = '' +var filename = '' +var strings = {} +var nodes = {} + +class Line: + var text = '' + var node_name = '' + var line_number = -1 + var file_name = '' + var implicit = false + var substitutions = [] + var meta = [] + + func _init(_text, _node_name, _line_number, _file_name, _implicit, _meta): + text = _text + node_name = _node_name + file_name = _file_name + implicit = _implicit + meta = _meta + + func clone(): + return get_script().new(text, node_name, line_number, file_name, implicit, meta) + + func _to_string(): + return '%s:%d: "%s"' % [file_name.get_file(), line_number, text] + +class Option: + var line + var id = -1 + var destination = '' + + func _init(_line, _id, _destination): + line = _line + id = _id + destination = _destination + + func clone(): + return get_script().new(self) + +class Command: + var command = '' + + func _init(_command): + command = _command + +class WolNode: + var name = '' + var instructions = [] + var labels = {} + var tags = [] + var source_id = '' + + func _init(other = null): + if other != null and other.get_script() == self.get_script(): + name = other.name + instructions += other.instructions + for key in other.labels.keys(): + labels[key] = other.labels[key] + tags += other.tags + source_id = other.source_id + + func equals(other): + if other.get_script() != get_script(): + return false + if other.name != name: + return false + if other.instructions != instructions: + return false + if other.labels != labels: + return false + if other.source_id != source_id: + return false + return true + + func _to_string(): + return "WolNode[%s:%s]" % [name, source_id] + +# TODO: Make this make sense +class Operand: + enum ValueType { + None, + StringValue, + BooleanValue, + FloatValue + } + + var value + var type + + func _init(_value): + if typeof(_value) == TYPE_OBJECT and _value.get_script() == get_script(): + set_value(_value.value) + else: + set_value(_value) + + func set_value(_value): + match typeof(_value): + TYPE_REAL,TYPE_INT: + set_number(_value) + TYPE_BOOL: + set_boolean(_value) + TYPE_STRING: + set_string(_value) + + func set_boolean(_value): + value = _value + type = ValueType.BooleanValue + return self + + func set_string(_value): + value = _value + type = ValueType.StringValue + return self + + func set_number(_value): + value = _value + type = ValueType.FloatValue + return self + + func clear_value(): + type = ValueType.None + value = null + + func clone(): + return get_script().new(self) + + func _to_string(): + return "Operand[%s:%s]" % [type, value] + +class Instruction: + var operation = -1 + var operands = [] + + func _init(other = null): + if other != null and other.get_script() == self.get_script(): + self.operation = other.operation + self.operands += other.operands + + func _to_string(): + return Constants.bytecode_name(operation) + ':' + operands as String + diff --git a/addons/Wol/core/Value.gd b/addons/Wol/core/Value.gd new file mode 100644 index 0000000..161df5a --- /dev/null +++ b/addons/Wol/core/Value.gd @@ -0,0 +1,137 @@ +extends Object + +const Constants = preload('res://addons/Wol/core/Constants.gd') + +const NANI = 'NaN' + +var type = Constants.ValueType.Nullean +var number = 0 +var string = '' +var variable = '' +var boolean = false + +func _init(value = NANI): + if typeof(value) == TYPE_OBJECT and value.get_script() == get_script(): + if value.type == Constants.ValueType.Variable: + type = value.type + variable = value.variable + else: + set_value(value) + +func value(): + match type: + Constants.ValueType.Number: + return number + Constants.ValueType.Str: + return string + Constants.ValueType.Boolean: + return boolean + Constants.ValueType.Variable: + return variable + return null + +func as_bool(): + match type: + Constants.ValueType.Number: + return number != 0 + Constants.ValueType.Str: + return !string.empty() + Constants.ValueType.Boolean: + return boolean + return false + +func as_string(): + return '%s' % value() + +func as_number(): + match type: + Constants.ValueType.Number: + return number + Constants.ValueType.Str: + return float(string) + Constants.ValueType.Boolean: + return 0.0 if !boolean else 1.0 + return .0 + +func set_value(value): + if value == null or (typeof(value) == TYPE_STRING and value == NANI): + type = Constants.ValueType.Nullean + return + + match typeof(value): + TYPE_INT, TYPE_REAL: + type = Constants.ValueType.Number + number = value + TYPE_STRING: + type = Constants.ValueType.Str + string = value + TYPE_BOOL: + type = Constants.ValueType.Boolean + boolean = value + +func add(other): + if type == Constants.ValueType.Str or other.type == Constants.ValueType.Str: + return get_script().new('%s%s' % [value(), other.value()]) + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return get_script().new(number + other.number) + return null + +func equals(other): + if other.get_script() != get_script(): + return false + if other.value() != value(): + return false + # TODO: Add more equality cases + return true + +func sub(other): + if type == Constants.ValueType.Str or other.type == Constants.ValueType.Str: + return get_script().new(str(value()).replace(str(other.value()),'')) + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return get_script().new(number - other.number) + return null + +func mult(other): + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return get_script().new(number * other.number) + return null + +func div(other): + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return get_script().new(number / other.number) + return null + +func mod(other): + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return get_script().new(number % other.number) + return null + +func negative(): + if type == Constants.ValueType.Number: + return get_script().new(-number) + return null + +func greater(other): + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return number > other.number + return false + +func less(other): + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return number < other.number + return false + +func geq(other): + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return number > other.number or equals(other) + return false + +func leq(other): + if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: + return number < other.number or equals(other) + return false + +func _to_string(): + return 'value(type[%s]: %s)' % [type,value()] + + diff --git a/addons/Wol/core/compiler/Compiler.gd b/addons/Wol/core/compiler/Compiler.gd new file mode 100644 index 0000000..617042d --- /dev/null +++ b/addons/Wol/core/compiler/Compiler.gd @@ -0,0 +1,461 @@ +extends Object + +signal error(message, line_number, column) + +const Constants = preload('res://addons/Wol/core/Constants.gd') +const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') +const Program = preload('res://addons/Wol/core/Program.gd') +const Parser = preload('res://addons/Wol/core/compiler/Parser.gd') + +const INVALID_TITLE = '[\\[<>\\]{}\\|:\\s#\\$]' + +var source = '' +var filename = '' + +var current_node +var has_implicit_string_tags = false +var soft_assert = false + +var string_count = 0 +var string_table = {} +var label_count = 0 + +func _init(_filename, _source = null, _soft_assert = false): + filename = _filename + soft_assert = _soft_assert + + if not _filename and _source: + self.source = _source + else: + var file = File.new() + file.open(_filename, File.READ) + self.source = file.get_as_text() + file.close() + + var source_lines = source.split('\n') + for i in range(source_lines.size()): + source_lines[i] = source_lines[i].strip_edges(false, true) + + source = source_lines.join('\n') + +func get_headers(offset = 0): + var header_property = RegEx.new() + var header_sep = RegEx.new() + + header_sep.compile('---(\r\n|\r|\n)') + header_property.compile('(?.*): *(?.*)') + + self.assert(header_sep.search(source), 'No headers found!') + + var title = '' + var position = Vector2.ZERO + + var source_lines = source.split('\n') + var line_number = offset + while line_number < source_lines.size(): + var line = source_lines[line_number] + line_number += 1 + + if not line.empty(): + var result = header_property.search(line) + + if result != null: + var field = result.get_string('field') + var value = result.get_string('value') + + if field == 'title': + var regex = RegEx.new() + regex.compile(INVALID_TITLE) + self.assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)]) + + title = value + + if field == 'position': + var regex = RegEx.new() + regex.compile('^position:.*,.*\\d$') + self.assert(regex.search(line), 'Couldn\'t parse position property in the headers, got "%s" instead in node "%s"' % [value, title]) + + position = Vector2(int(value.split(',')[0].strip_edges()), int(value.split(',')[1].strip_edges())) + + # TODO: Implement color and tags + + if line == '---': + break + + return { + 'title': title, + 'position': position + } + +func get_body(offset = 0): + var body_lines = [] + + var source_lines = source.split('\n') + var recording = false + var line_number = offset + + while line_number < source_lines.size() and source_lines[line_number] != '===': + if recording: + body_lines.append(source_lines[line_number]) + + recording = recording or source_lines[line_number] == '---' + line_number += 1 + + line_number += 1 + + return PoolStringArray(body_lines).join('\n') + +func get_nodes(): + var nodes = [] + var line_number = 0 + var source_lines = source.split('\n') + while line_number < source_lines.size(): + var headers = get_headers(line_number) + var body = get_body(line_number) + headers.body = body + + nodes.append(headers) + + # Add +2 to the final line to skip the === from that node + line_number = Array(source_lines).find_last(body.split('\n')[-1]) + 2 + + while line_number < source_lines.size() and source_lines[line_number].empty(): + line_number += 1 + + return nodes + +func assert(statement, message, line_number = -1, column = -1, _absolute_line_number = -1): + if not soft_assert: + assert(statement, message + ('; on line %d column %d' % [line_number, column])) + elif not statement: + emit_signal('error', message, line_number, column) + +func compile(): + var parsed_nodes = [] + for node in get_nodes(): + var lexer = Lexer.new(self, filename, node.title, node.body) + var tokens = lexer.tokenize() + + # In case of lexer error + if not tokens: + return + + var parser = Parser.new(self, node.title, tokens) + var parser_node = parser.parse_node() + + parser_node.name = node.title + parsed_nodes.append(parser_node) + + var program = Program.new() + program.filename = filename + + for node in parsed_nodes: + compile_node(program, node) + + for key in string_table: + program.strings[key] = string_table[key] + + return program + +func compile_node(program, parsed_node): + self.assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name) + + var node_compiled = Program.WolNode.new() + + node_compiled.name = parsed_node.name + node_compiled.tags = parsed_node.tags + + if parsed_node.source != null and not parsed_node.source.empty(): + node_compiled.source_id = register_string( + parsed_node.source, + parsed_node.name, + 'line:' + parsed_node.name, + 0, + [] + ) + else: + var start_label = register_label() + emit(Constants.ByteCode.Label, node_compiled, [Program.Operand.new(start_label)]) + + for statement in parsed_node.statements: + generate_statement(node_compiled, statement) + + var dangling_options = false + for instruction in node_compiled.instructions: + if instruction.operation == Constants.ByteCode.AddOption: + dangling_options = true + if instruction.operation == Constants.ByteCode.ShowOptions: + dangling_options = false + + if dangling_options: + emit(Constants.ByteCode.ShowOptions, node_compiled) + emit(Constants.ByteCode.RunNode, node_compiled) + else: + emit(Constants.ByteCode.Stop, node_compiled) + + program.nodes[node_compiled.name] = node_compiled + +func register_string(text, node_name, id = '', line_number = -1, tags = []): + var line_id_used = '' + var implicit = false + + if id.empty(): + line_id_used = '%s-%s-%d' % [filename, node_name, string_count] + string_count += 1 + + #use this when we generate implicit tags + #they are not saved and are generated + #aka dummy tags that change on each compilation + has_implicit_string_tags = true + + implicit = true + else : + line_id_used = id + implicit = false + + var string_info = Program.Line.new(text, node_name, line_number, filename, implicit, tags) + string_table[line_id_used] = string_info + + return line_id_used + +func register_label(comment = ''): + label_count += 1 + return 'Label%s%s' % [label_count, comment] + +func emit(bytecode, node = current_node, operands = []): + var instruction = Program.Instruction.new(null) + instruction.operation = bytecode + instruction.operands = operands + + if node == null: + printerr('Trying to emit to null node with byte_code: %s' % bytecode) + return + + node.instructions.append(instruction) + + if bytecode == Constants.ByteCode.Label: + node.labels[instruction.operands[0].value] = node.instructions.size() - 1 + +func generate_statement(node, statement): + match statement.type: + Constants.StatementTypes.CustomCommand: + generate_custom_command(node, statement.custom_command) + + Constants.StatementTypes.ShortcutOptionGroup: + generate_shortcut_group(node, statement.shortcut_option_group) + + Constants.StatementTypes.Block: + generate_block(node, statement.block.statements) + + Constants.StatementTypes.IfStatement: + generate_if(node, statement.if_statement) + + Constants.StatementTypes.OptionStatement: + generate_option(node, statement.option_statement) + + Constants.StatementTypes.AssignmentStatement: + generate_assignment(node, statement.assignment) + + Constants.StatementTypes.Line: + generate_line(node, statement) + _: + self.assert(false, statement.line_number, 'Illegal statement type [%s]. Could not generate code.' % statement.type) + +func generate_custom_command(node, command): + # TODO: See if the first tree of this statement is being used + if command.expression != null: + generate_expression(node, command.expression) + else: + var command_string = command.client_command + if command_string == 'stop': + emit(Constants.ByteCode.Stop, node) + else : + emit(Constants.ByteCode.RunCommand, node, [Program.Operand.new(command_string)]) + +func generate_line(node, statement): + # TODO: Implement proper line numbers (global and local) + var line = statement.line + var expression_count = line.substitutions.size() + + while not line.substitutions.empty(): + var inline_expression = line.substitutions.pop_back() + generate_expression(node, inline_expression.expression) + + var num = register_string(line.line_text, node.name, line.line_id, statement.line_number, line.tags); + emit(Constants.ByteCode.RunLine, node,[Program.Operand.new(num), Program.Operand.new(expression_count)]) + +func generate_shortcut_group(node, shortcut_group): + var end = register_label('group_end') + var labels = [] + var option_count = 0 + + for option in shortcut_group.options: + var endof_clause = '' + var op_destination = register_label('option_%s' % [option_count + 1]) + + labels.append(op_destination) + + if option.condition != null: + endof_clause = register_label('conditional_%s' % option_count) + generate_expression(node, option.condition) + emit(Constants.ByteCode.JumpIfFalse, node, [Program.Operand.new(endof_clause)]) + + var label_line_id = '' #TODO: Add tag support + var label_string_id = register_string( + option.label, + node.name, + label_line_id, + option.line_number, + [] + ) + + emit(Constants.ByteCode.AddOption, node, [Program.Operand.new(label_string_id), Program.Operand.new(op_destination)]) + + if option.condition != null: + emit(Constants.ByteCode.Label, node, [Program.Operand.new(endof_clause)]) + emit(Constants.ByteCode.Pop, node) + + option_count += 1 + + emit(Constants.ByteCode.ShowOptions, node) + emit(Constants.ByteCode.Jump, node) + + option_count = 0 + + for option in shortcut_group.options: + emit(Constants.ByteCode.Label, node, [Program.Operand.new(labels[option_count])]) + + if option.node != null: + generate_block(node, option.node.statements) + emit(Constants.ByteCode.JumpTo, node, [Program.Operand.new(end)]) + option_count += 1 + + emit(Constants.ByteCode.Label, node, [Program.Operand.new(end)]) + emit(Constants.ByteCode.Pop, node) + +func generate_block(node, statements = []): + if not statements.empty(): + for statement in statements: + generate_statement(node, statement) + + +func generate_if(node, if_statement): + var endif = register_label('endif') + + for clause in if_statement.clauses: + var end_clause = register_label('skip_clause') + + if clause.expression != null: + generate_expression(node, clause.expression) + emit(Constants.ByteCode.JumpIfFalse, node, [Program.Operand.new(end_clause)]) + + generate_block(node, clause.statements) + emit(Constants.ByteCode.JumpTo, node, [Program.Operand.new(endif)]) + + if clause.expression != null: + emit(Constants.ByteCode.Label, node, [Program.Operand.new(end_clause)]) + + if clause.expression != null: + emit(Constants.ByteCode.Pop) + + emit(Constants.ByteCode.Label, node, [Program.Operand.new(endif)]) + +func generate_option(node, option): + var destination = option.destination + + if option.label == null or option.label.empty(): + emit(Constants.ByteCode.RunNode, node, [Program.Operand.new(destination)]) + else : + var line_id = '' #TODO: ADD TAG SUPPORT + var string_id = register_string(option.label, node.name, line_id, option.line_number, []) + + emit(Constants.ByteCode.AddOption, node, [Program.Operand.new(string_id), Program.Operand.new(destination)]) + +func generate_assignment(node, assignment): + if assignment.operation == Constants.TokenType.EqualToOrAssign: + generate_expression(node, assignment.value) + else : + emit(Constants.ByteCode.PushVariable, node, [assignment.destination]) + generate_expression(node, assignment.value) + + match assignment.operation: + Constants.TokenType.AddAssign: + emit( + Constants.ByteCode.CallFunc, + node, + [Program.Operand.new(Constants.token_type_name(Constants.TokenType.Add))] + ) + Constants.TokenType.MinusAssign: + emit( + Constants.ByteCode.CallFunc, + node, + [Program.Operand.new(Constants.token_type_name(Constants.TokenType.Minus))] + ) + Constants.TokenType.MultiplyAssign: + emit( + Constants.ByteCode.CallFunc, + node, + [Program.Operand.new(Constants.token_type_name(Constants.TokenType.MultiplyAssign))] + ) + Constants.TokenType.DivideAssign: + emit( + Constants.ByteCode.CallFunc, + node, + [Program.Operand.new(Constants.token_type_name(Constants.TokenType.DivideAssign))] + ) + _: + printerr('Unable to generate assignment') + + emit(Constants.ByteCode.StoreVariable, node, [Program.Operand.new(assignment.destination)]) + emit(Constants.ByteCode.Pop, node) + +func generate_expression(node, expression): + match expression.type: + Constants.ExpressionType.Value: + generate_value(node, expression.value) + Constants.ExpressionType.FunctionCall: + for parameter in expression.parameters: + generate_expression(node, parameter) + + emit(Constants.ByteCode.PushNumber, node, [Program.Operand.new(expression.parameters.size())]) + emit(Constants.ByteCode.CallFunc, node, [Program.Operand.new(expression.function)]) + _: + printerr('No expression.') + +func generate_value(node, value): + match value.value.type: + Constants.ValueType.Number: + emit( + Constants.ByteCode.PushNumber, + node, + [Program.Operand.new(value.value.as_number())] + ) + Constants.ValueType.Str: + var id = register_string( + value.value.as_string(), + node.name, + '', + value.line_number, + [] + ) + emit( + Constants.ByteCode.PushString, + node, + [Program.Operand.new(id)] + ) + Constants.ValueType.Boolean: + emit( + Constants.ByteCode.PushBool, + node, + [Program.Operand.new(value.value.as_bool())] + ) + Constants.ValueType.Variable: + emit( + Constants.ByteCode.PushVariable, + node, + [Program.Operand.new(value.value.variable)] + ) + Constants.ValueType.Nullean: + emit(Constants.ByteCode.PushNull, node) + _: + printerr('Unrecognized valuenode type: %s' % value.value.type) diff --git a/addons/Wol/core/compiler/Lexer.gd b/addons/Wol/core/compiler/Lexer.gd new file mode 100644 index 0000000..b651b99 --- /dev/null +++ b/addons/Wol/core/compiler/Lexer.gd @@ -0,0 +1,485 @@ +extends Object + +const Constants = preload('res://addons/Wol/core/Constants.gd') + +const LINE_COMMENT = '//' +const FORWARD_SLASH = '/' +const LINE_SEPARATOR = '\n' + +const BASE = 'base' +const DASH = '-' +const COMMAND = 'command' +const LINK = 'link' +const SHORTCUT = 'shortcut' +const TAG = 'tag' +const EXPRESSION = 'expression' +const ASSIGNMENT = 'assignment' +const OPTION = 'option' +const OR = 'or' +const DESTINATION = 'destination' +const INLINE = 'inline' +const FORMAT_FUNCTION = 'format' + +var WHITESPACE = '\\s*' + +var compiler +var filename = '' +var title = '' +var text = '' + +var states = {} +var default_state +var current_state + +var indent_stack = [] +var should_track_indent = false + +func _init(_compiler, _filename, _title, _text): + create_states() + + compiler = _compiler + filename = _filename + title = _title + text = _text + +func create_states(): + var patterns = {} + patterns[Constants.TokenType.Text] = ['.*', 'any text'] + + patterns[Constants.TokenType.Number] = ['\\-?[0-9]+(\\.[0-9+])?', 'any number'] + patterns[Constants.TokenType.Str] = ['\"([^\"\\\\]*(?:\\.[^\"\\\\]*)*)\"', 'any text'] + patterns[Constants.TokenType.TagMarker] = ['\\#', 'a tag #'] + patterns[Constants.TokenType.LeftParen] = ['\\(', 'left parenthesis ('] + patterns[Constants.TokenType.RightParen] = ['\\)', 'right parenthesis )'] + patterns[Constants.TokenType.EqualTo] = ['(==|is(?!\\w)|eq(?!\\w))', '"=", "is" or "eq"'] + patterns[Constants.TokenType.EqualToOrAssign] = ['(=|to(?!\\w))', '"=" or "to"'] + patterns[Constants.TokenType.NotEqualTo] = ['(\\!=|neq(?!\\w))', '"!=" or "neq"'] + patterns[Constants.TokenType.GreaterThanOrEqualTo] = ['(\\>=|gte(?!\\w))', '">=" or "gte"'] + patterns[Constants.TokenType.GreaterThan] = ['(\\>|gt(?!\\w))', '">" or "gt"'] + patterns[Constants.TokenType.LessThanOrEqualTo] = ['(\\<=|lte(?!\\w))', '"<=" or "lte"'] + patterns[Constants.TokenType.LessThan] = ['(\\<|lt(?!\\w))', '"<" or "lt"'] + patterns[Constants.TokenType.AddAssign] = ['\\+=', '"+="'] + patterns[Constants.TokenType.MinusAssign] = ['\\-=', '"-="'] + patterns[Constants.TokenType.MultiplyAssign] = ['\\*=', '"*="'] + patterns[Constants.TokenType.DivideAssign] = ['\\/=', '"/="'] + patterns[Constants.TokenType.Add] = ['\\+', '"+"'] + patterns[Constants.TokenType.Minus] = ['\\-', '"-"'] + patterns[Constants.TokenType.Multiply] = ['\\*', '"*"'] + patterns[Constants.TokenType.Divide] = ['\\/', '"/"'] + patterns[Constants.TokenType.Modulo] = ['\\%', '"%"'] + patterns[Constants.TokenType.And] = ['(\\&\\&|and(?!\\w))', '"&&" or "and"'] + patterns[Constants.TokenType.Or] = ['(\\|\\||or(?!\\w))', '"||" or "or"'] + patterns[Constants.TokenType.Xor] = ['(\\^|xor(?!\\w))', '"^" or "xor"'] + patterns[Constants.TokenType.Not] = ['(\\!|not(?!\\w))', '"!" or "not"'] + patterns[Constants.TokenType.Variable] = ['\\$([A-Za-z0-9_\\.])+', 'any variable'] + patterns[Constants.TokenType.Comma] = ['\\,', '","'] + patterns[Constants.TokenType.TrueToken] = ['true(?!\\w)', '"true"'] + patterns[Constants.TokenType.FalseToken] = ['false(?!\\w)', '"false"'] + patterns[Constants.TokenType.NullToken] = ['null(?!\\w)', '"null"'] + patterns[Constants.TokenType.BeginCommand] = ['\\<\\<', 'beginning of a command "<<"'] + patterns[Constants.TokenType.EndCommand] = ['\\>\\>', 'ending of a command ">>"'] + patterns[Constants.TokenType.OptionStart] = ['\\[\\[', 'start of an option "[["'] + patterns[Constants.TokenType.OptionEnd] = ['\\]\\]', 'end of an option "]]"'] + patterns[Constants.TokenType.OptionDelimit] = ['\\|', 'middle of an option "|"'] + patterns[Constants.TokenType.Identifier] = ['[a-zA-Z0-9_:\\.]+', 'any reference to another node'] + patterns[Constants.TokenType.IfToken] = ['if(?!\\w)', '"if"'] + patterns[Constants.TokenType.ElseToken] = ['else(?!\\w)', '"else"'] + patterns[Constants.TokenType.ElseIf] = ['elseif(?!\\w)', '"elseif"'] + patterns[Constants.TokenType.EndIf] = ['endif(?!\\w)', '"endif"'] + patterns[Constants.TokenType.Set] = ['set(?!\\w)', '"set"'] + patterns[Constants.TokenType.ShortcutOption] = ['\\-\\>\\s*', '"->"'] + patterns[Constants.TokenType.ExpressionFunctionStart] = ['\\{', '"{"'] + patterns[Constants.TokenType.ExpressionFunctionEnd] = ['\\}', '"}"'] + patterns[Constants.TokenType.FormatFunctionStart] = ['(? previous_indentation: + indent_stack.push_front([indentation, true]) + + var indent = Token.new( + Constants.TokenType.Indent, + current_state, + filename, + line_number, + previous_indentation + ) + indent.value = '%*s' % [indentation - previous_indentation, ''] + + should_track_indent = false + token_stack.push_front(indent) + + elif indentation < previous_indentation: + while indentation < indent_stack.front()[0]: + var top = indent_stack.pop_front()[1] + if top: + var deindent = Token.new(Constants.TokenType.Dedent, current_state, line_number, 0) + token_stack.push_front(deindent) + + var column = indentation + var whitespace = RegEx.new() + whitespace.compile(WHITESPACE) + + while column < fresh_line.length(): + if fresh_line.substr(column).begins_with(LINE_COMMENT): + break + + var matched = false + + for rule in current_state.rules: + var found = rule.regex.search(fresh_line, column) + + if !found: + continue + + var token_text = '' + + # NOTE: If this is text then we back up to the most recent delimiting token + # and treat everything from there as text. + if rule.token_type == Constants.TokenType.Text: + + var start_index = indentation + + if token_stack.size() > 0 : + while token_stack.front().type == Constants.TokenType.Identifier: + token_stack.pop_front() + + var start_delimit_token = token_stack.front() + start_index = start_delimit_token.column + + if start_delimit_token.type == Constants.TokenType.Indent: + start_index += start_delimit_token.value.length() + if start_delimit_token.type == Constants.TokenType.Dedent: + start_index = indentation + + column = start_index + var end_index = found.get_start() + found.get_string().length() + + token_text = fresh_line.substr(start_index, end_index - start_index) + else: + token_text = found.get_string() + + column += token_text.length() + + if rule.token_type == Constants.TokenType.Str: + token_text = token_text.substr(1, token_text.length() - 2) + token_text = token_text.replace('\\\\', '\\') + token_text = token_text.replace('\\\'','\'') + + var token = Token.new( + rule.token_type, + current_state, + filename, + line_number, + column, + token_text + ) + token.delimits_text = rule.delimits_text + + token_stack.push_front(token) + + if rule.enter_state != null and rule.enter_state.length() > 0: + if not states.has(rule.enter_state): + printerr('State[%s] not known - line(%s) col(%s)' % [rule.enter_state, line_number, column]) + return [] + + enter_state(states[rule.enter_state]) + + if should_track_indent: + if indent_stack.front()[0] < indentation: + indent_stack.append([indentation, false]) + + matched = true + break + + if not matched: + var rules = [] + for rule in current_state.rules: + rules.append('"%s" (%s)' % [Constants.token_type_name(rule.token_type), rule.human_readable_identifier]) + + var error_data = [ + PoolStringArray(rules).join(', ') if rules.size() == 1 else PoolStringArray(rules.slice(0, rules.size() - 2)).join(', ') + ' or %s' % rules[-1], + filename, + title, + line_number, + column + ] + compiler.assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data, line_number, column) + return + + var last_whitespace = whitespace.search(line, column) + if last_whitespace: + column += last_whitespace.get_string().length() + + token_stack.invert() + + return token_stack + +func line_indentation(line): + var indent_regex = RegEx.new() + indent_regex.compile('^(\\s*)') + + var found = indent_regex.search(line) + + if !found or found.get_string().length() <= 0: + return 0 + + return found.get_string().length() + +func enter_state(state): + current_state = state; + if current_state.track_indent: + should_track_indent = true + +class Token: + var type = -1 + var value = '' + + var filename = '' + var line_number = -1 + var column = -1 + var text = '' + + var delimits_text = false + var parameter_count = -1 + var lexer_state = '' + + func _init(_type, _state, _filename, _line_number = -1, _column = -1, _value = ''): + type = _type + lexer_state = _state.name + filename = _filename + line_number = _line_number + column = _column + value = _value + + func _to_string(): + return '%s (%s) at %s:%s (state: %s)' % [Constants.token_type_name(type), value, line_number, column, lexer_state] + +class LexerState: + var name = '' + var patterns = {} + var rules = [] + var track_indent = false + + func _init(_patterns): + patterns = _patterns + + func add_transition(type, state = '', delimit_text = false): + var pattern = '\\G%s' % patterns[type][0] + var rule = Rule.new(type, pattern, patterns[type][1], state, delimit_text) + rules.append(rule) + return rule + + func add_text_rule(type, state = ''): + if contains_text_rule() : + printerr('State already contains Text rule') + return null + + var delimiters:Array = [] + for rule in rules: + if rule.delimits_text: + delimiters.append('%s' % rule.regex.get_pattern().substr(2)) + + var pattern = '\\G((?!%s).)*' % [PoolStringArray(delimiters).join('|')] + var rule = add_transition(type, state) + rule.regex = RegEx.new() + rule.regex.compile(pattern) + rule.is_text_rule = true + return rule + + func contains_text_rule(): + for rule in rules: + if rule.is_text_rule: + return true + return false + +class Rule: + var regex + + var enter_state = '' + var token_type = -1 + var is_text_rule = false + var delimits_text = false + var human_readable_identifier = '' + + func _init(_type, _regex, _human_readable_identifier, _enter_state, _delimits_text): + token_type = _type + + regex = RegEx.new() + regex.compile(_regex) + + human_readable_identifier = _human_readable_identifier + enter_state = _enter_state + delimits_text = _delimits_text + + func _to_string(): + return '[Rule : %s (%s) - %s]' % [Constants.token_type_name(token_type), human_readable_identifier, regex] diff --git a/addons/Wol/core/compiler/Parser.gd b/addons/Wol/core/compiler/Parser.gd new file mode 100644 index 0000000..b4385eb --- /dev/null +++ b/addons/Wol/core/compiler/Parser.gd @@ -0,0 +1,1065 @@ +extends Object + +# warnings-disable + +const Constants = preload('res://addons/Wol/core/Constants.gd') +const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') +const Value = preload('res://addons/Wol/core/Value.gd') + +var compiler +var title = '' +var tokens = [] + +func _init(_compiler, _title, _tokens): + compiler = _compiler + title = _title + tokens = _tokens + +enum Associativity { + Left, + Right, + None +} + +func parse_node(): + return WolNode.new('Start', null, self) + +func next_symbol_is(valid_types): + return tokens.front().type in valid_types + +# NOTE: 0 look ahead for `<<` and `else` +func next_symbols_are(valid_types): + var temporary = [] + tokens + for type in valid_types: + if temporary.pop_front().type != type: + return false + return true + +func expect_symbol(token_types = []): + var token = tokens.pop_front() as Lexer.Token + + if token_types.size() == 0: + if token.type == Constants.TokenType.EndOfInput: + compiler.assert(false, 'Unexpected end of input') + return token + + for type in token_types: + if token.type == type: + return token + + var token_names = [] + for type in token_types: + token_names.append(Constants.token_type_name(type)) + + var error_guess = '\n' + + if Constants.token_type_name(token.type) == 'Identifier' \ + and Constants.token_type_name(token_types[0]) == 'OptionEnd': + error_guess += 'Does the node your refer to have a space in it?' + else: + error_guess = '' + + var error_data = [ + PoolStringArray(token_names).join(', '), + Constants.token_type_name(token.type), + error_guess + ] + compiler.assert(false, 'Expected token "%s" but got "%s"%s' % error_data, token.line_number, token.column) + return token + +static func tab(indent_level, input, newline = true): + return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n'] + +class ParseNode: + var name = '' + + var parent + var line_number = -1 + var tags = [] + + func _init(_parent, _parser): + parent = _parent + + var tokens = _parser.tokens as Array + if tokens.size() > 0: + line_number = tokens.front().line_number + + tags = [] + + func tree_string(_indent_level): + return 'Not_implemented' + + func tags_to_string(_indent_level): + return 'TAGSNOTIMPLEMENTED' + + func get_node_parent(): + var node = self + while node != null: + if node is ParseNode: + return node as WolNode + node = node.parent + return null + + func tab(indent_level, input, newline = true): + return '%*s| %s%s' % [ indent_level * 2, '', input, '' if !newline else '\n'] + +class WolNode extends ParseNode: + var source = '' + + var editor_node_tags = [] + var statements = [] + var parser + + func _init(_name, parent, _parser).(parent, _parser): + name = _name + parser = _parser + while parser.tokens.size() > 0 \ + and not parser.next_symbol_is([Constants.TokenType.Dedent, Constants.TokenType.EndOfInput]): + + parser.compiler.assert( + not parser.next_symbol_is([Constants.TokenType.Indent]), + 'Found a stray indentation!', + parser.tokens.front().line_number, + parser.tokens.front().column + ) + + statements.append(Statement.new(self, parser)) + + func tree_string(indent_level): + var info = [] + for statement in statements: + info.append(statement.tree_string(indent_level + 1)) + + return PoolStringArray(info).join('') + +class Statement extends ParseNode: + var Type = Constants.StatementTypes + + var type = -1 + var block + var if_statement + var option_statement + var assignment + var shortcut_option_group + var custom_command + var line + + func _init(parent, parser).(parent, parser): + if Block.can_parse(parser): + block = Block.new(self, parser) + type = Type.Block + + elif IfStatement.can_parse(parser): + if_statement = IfStatement.new(self, parser) + type = Type.IfStatement + + elif OptionStatement.can_parse(parser): + option_statement = OptionStatement.new(self, parser) + type = Type.OptionStatement + + elif Assignment.can_parse(parser): + assignment = Assignment.new(self, parser) + type = Type.AssignmentStatement + + elif ShortcutOptionGroup.can_parse(parser): + shortcut_option_group = ShortcutOptionGroup.new(self, parser) + type = Type.ShortcutOptionGroup + + elif CustomCommand.can_parse(parser): + custom_command = CustomCommand.new(self, parser) + type = Type.CustomCommand + + elif parser.next_symbol_is([Constants.TokenType.Text]): + line = LineNode.new(self, parser) + type = Type.Line + + else: + parser.compiler.assert(false, 'Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string()) + + var tags = [] + + while parser.next_symbol_is([Constants.TokenType.TagMarker]): + parser.expect_symbol([Constants.TokenType.TagMarker]) + var tag = parser.expect_symbol([Constants.TokenType.Identifier]).value + tags.append(tag) + + if tags.size() > 0: + self.tags = tags + + func tree_string(indent_level): + var info = [] + + match type: + Type.Block: + info.append(block.tree_string(indent_level)) + Type.IfStatement: + info.append(if_statement.tree_string(indent_level)) + Type.AssignmentStatement: + info.append(assignment.tree_string(indent_level)) + Type.OptionStatement: + info.append(option_statement.tree_string(indent_level)) + Type.ShortcutOptionGroup: + info.append(shortcut_option_group.tree_string(indent_level)) + Type.CustomCommand: + info.append(custom_command.tree_string(indent_level)) + Type.Line: + info.append(line.tree_string(indent_level)) + _: + printerr('Cannot print statement') + + return PoolStringArray(info).join('') + +class InlineExpression extends ParseNode: + var expression + + func _init(parent, parser).(parent, parser): + parser.expect_symbol([Constants.TokenType.ExpressionFunctionStart]) + expression = ExpressionNode.parse(self, parser) + parser.expect_symbol([Constants.TokenType.ExpressionFunctionEnd]) + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.ExpressionFunctionStart]) + + func tree_string(_indent_level): + return "InlineExpression:" + +# Returns a format_text string as [ name "{0}" key1="value1" key2="value2" ] +class FormatFunctionNode extends ParseNode: + var format_text = '' + var expression_value + + func _init(parent:ParseNode, parser, expressionCount:int).(parent, parser): + format_text="[" + parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) + + # FIXME: Add exit condition in case of failure + while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): + if parser.next_symbol_is([Constants.TokenType.Text]): + format_text += parser.expect_symbol().value + + if InlineExpression.can_parse(parser): + expression_value = InlineExpression.new(self, parser) + format_text +=" \"{%d}\" " % expressionCount + parser.expect_symbol() + format_text+="]" + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.FormatFunctionStart]) + + # TODO: Make format prettier and add more information + func tree_string(_indent_level): + return "FormatFunction" + +class LineNode extends ParseNode: + var line_text = '' + # FIXME: Right now we are putting the formatfunctions and inline expressions in the same + # list but if at some point we want to strongly type our sub list we need to make a new + # parse node that can have either an InlineExpression or a FunctionFormat + # .. This is a consideration for Godot4.x + var substitutions = [] + var line_id = '' + var line_tags = [] + + # NOTE: If format function an inline functions are both present + # returns a line in the format "Some text {0} and some other {1}[format "{2}" key="value" key="value"]" + + func _init(parent, parser).(parent, parser): + while parser.next_symbol_is( + [ + Constants.TokenType.FormatFunctionStart, + Constants.TokenType.ExpressionFunctionStart, + Constants.TokenType.Text, + Constants.TokenType.TagMarker + ] + ): + + if FormatFunctionNode.can_parse(parser): + var format_function = FormatFunctionNode.new(self, parser, substitutions.size()) + if format_function.expression_value != null: + substitutions.append(format_function.expression_value) + + line_text += format_function.format_text + + elif InlineExpression.can_parse(parser): + var inline_expression = InlineExpression.new(self, parser) + line_text += '{%d}' % substitutions.size() + substitutions.append(inline_expression) + + elif parser.next_symbols_are([Constants.TokenType.TagMarker, Constants.TokenType.Identifier]): + parser.expect_symbol() + var tag_token = parser.expect_symbol([ Constants.TokenType.Identifier ]) + if tag_token.value.begins_with("line:"): + if line_id.empty(): + line_id = tag_token.value + else: + printerr("Too many line_tags @[%s:%d]" % [parser.currentNodeName, tag_token.line_number]) + return + else: + tags.append(tag_token.value) + + else: + var token = parser.expect_symbol() + if token.line_number == line_number and token.type != Constants.TokenType.BeginCommand: + line_text += token.value + else: + parser.tokens.push_front(token) + break + + + func tree_string(indent_level): + return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()]) + + +class CustomCommand extends ParseNode: + + enum Type { + Expression, + ClientCommand + } + + var type = -1 + var expression + var client_command + + func _init(parent, parser).(parent, parser): + parser.expect_symbol([Constants.TokenType.BeginCommand]) + + var command_tokens = [] + command_tokens.append(parser.expect_symbol()) + + # FIXME: add exit condition + while not parser.next_symbol_is([Constants.TokenType.EndCommand]): + command_tokens.append(parser.expect_symbol()) + + parser.expect_symbol([Constants.TokenType.EndCommand]) + + #if first token is identifier and second is leftt parenthesis + #evaluate as function + if command_tokens.size() > 1 \ + and command_tokens[0].type == Constants.TokenType.Identifier \ + and command_tokens[1].type == Constants.TokenType.LeftParen: + + var p = get_script().new(command_tokens, parser.library) + expression = ExpressionNode.parse(self, p) + type = Type.Expression + + else: + # otherwise evaluate command + type = Type.ClientCommand + client_command = command_tokens[0].value + + func tree_string(indent_level): + match type: + Type.Expression: + return tab(indent_level, 'Expression: %s' % expression.tree_string(indent_level+1)) + Type.ClientCommand: + return tab(indent_level, 'Command: %s' % client_command) + return '' + + static func can_parse(parser): + return (parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.Text]) + or parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.Identifier])) + +class ShortcutOptionGroup extends ParseNode: + var options = [] + + func _init(parent, parser).(parent, parser): + + # parse options until there is no more + # expect one otherwise invalid + + var index = 0 + while parser.next_symbol_is([Constants.TokenType.ShortcutOption]): + options.append(ShortCutOption.new(index, self, parser)) + index += 1 + + func tree_string(indent_level): + var info = [] + + info.append(tab(indent_level, 'Shortcut Option Group{')) + + for option in options: + info.append(option.tree_string(indent_level+1)) + + info.append(tab(indent_level, '}')) + + return PoolStringArray(info).join('') + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.ShortcutOption]) + +class ShortCutOption extends ParseNode: + var label = '' + var condition + var node + + func _init(index, parent, parser).(parent, parser): + parser.expect_symbol([Constants.TokenType.ShortcutOption]) + label = parser.expect_symbol([Constants.TokenType.Text]).value + + # FIXME: Parse the conditional << if $x >> when it exists + var tags = [] + while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \ + or parser.next_symbol_is([Constants.TokenType.TagMarker]): + + if parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]): + parser.expect_symbol([Constants.TokenType.BeginCommand]) + parser.expect_symbol([Constants.TokenType.IfToken]) + condition = ExpressionNode.parse(self, parser) + parser.expect_symbol([Constants.TokenType.EndCommand]) + + elif parser.next_symbol_is([Constants.TokenType.TagMarker]): + parser.expect_symbol([Constants.TokenType.TagMarker]) + var tag = parser.expect_symbol([Constants.TokenType.Identifier]).value + tags.append(tag) + + + self.tags = tags + # parse remaining statements + + if parser.next_symbol_is([Constants.TokenType.Indent]): + parser.expect_symbol([Constants.TokenType.Indent]) + node = WolNode.new('%s.%s' % [parent.name, index], self, parser) + parser.expect_symbol([Constants.TokenType.Dedent]) + + + func tree_string(indent_level): + var info = [] + + info.append(tab(indent_level, 'Option \'%s\'' % label)) + + if condition != null: + info.append(tab(indent_level + 1, '(when:')) + info.append(condition.tree_string(indent_level + 2)) + info.append(tab(indent_level + 1, '),')) + + if node != null: + info.append(tab(indent_level, '{')) + info.append(node.tree_string(indent_level + 1)) + info.append(tab(indent_level, '}')) + + return PoolStringArray(info).join('') + +#Blocks are groups of statements with the same indent level +class Block extends ParseNode: + + var statements = [] + + func _init(parent, parser).(parent, parser): + #read indent + parser.expect_symbol([Constants.TokenType.Indent]) + + #keep reading statements until we hit a dedent + # FIXME: find exit condition + while not parser.next_symbol_is([Constants.TokenType.Dedent]): + #parse all statements including nested blocks + statements.append(Statement.new(self, parser)) + + #clean up dedent + parser.expect_symbol([Constants.TokenType.Dedent]) + + + func tree_string(indent_level): + var info = [] + + info.append(tab(indent_level, 'Block {')) + + for statement in statements: + info.append(statement.tree_string(indent_level + 1)) + + info.append(tab(indent_level, '}')) + + return PoolStringArray(info).join('') + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.Indent]) + +# NOTE: Option Statements are links to other nodes +class OptionStatement extends ParseNode: + var destination = '' + var label = '' + + func _init(parent, parser).(parent, parser): + var strings = [] + + # NOTE: parse [[LABEL + parser.expect_symbol([Constants.TokenType.OptionStart]) + strings.append(parser.expect_symbol([Constants.TokenType.Text]).value) + + # NOTE: if there is a | get the next string + if parser.next_symbol_is([Constants.TokenType.OptionDelimit]): + parser.expect_symbol([Constants.TokenType.OptionDelimit]) + var t = parser.expect_symbol([Constants.TokenType.Text, Constants.TokenType.Identifier]) + + strings.append(t.value as String) + + label = strings[0] if strings.size() > 1 else '' + destination = strings[1] if strings.size() > 1 else strings[0] + + parser.expect_symbol([Constants.TokenType.OptionEnd]) + + func tree_string(indent_level): + if label != null: + return tab(indent_level, 'Option: %s -> %s' % [label, destination]) + else: + return tab(indent_level, 'Option: -> %s' % destination) + + static func can_parse(parser): + return parser.next_symbol_is([Constants.TokenType.OptionStart]) + +class IfStatement extends ParseNode: + var clauses = []# + + func _init(parent, parser).(parent, parser): + + #<> + var prime = Clause.new() + + parser.expect_symbol([Constants.TokenType.BeginCommand]) + parser.expect_symbol([Constants.TokenType.IfToken]) + prime.expression = ExpressionNode.parse(self, parser) + parser.expect_symbol([Constants.TokenType.EndCommand]) + + #read statements until 'endif' or 'else' or 'else if' + var statements = []#statement + while not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.EndIf]) \ + and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseToken]) \ + and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseIf]): + + statements.append(Statement.new(self, parser)) + + #ignore dedent + while parser.next_symbol_is([Constants.TokenType.Dedent]): + parser.expect_symbol([Constants.TokenType.Dedent]) + + prime.statements = statements + clauses.append(prime) + + #handle all else if + while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseIf]): + var clause_elif = Clause.new() + + #parse condition syntax + parser.expect_symbol([Constants.TokenType.BeginCommand]) + parser.expect_symbol([Constants.TokenType.ElseIf]) + clause_elif.expression = ExpressionNode.parse(self, parser) + parser.expect_symbol([Constants.TokenType.EndCommand]) + + + var elif_statements = []#statement + while not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.EndIf]) \ + and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseToken]) \ + and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseIf]): + + elif_statements.append(Statement.new(self, parser)) + + #ignore dedent + while parser.next_symbol_is([Constants.TokenType.Dedent]): + parser.expect_symbol([Constants.TokenType.Dedent]) + + clause_elif.statements = statements + clauses.append(clause_elif) + + #handle else if exists + if (parser.next_symbols_are([Constants.TokenType.BeginCommand, + Constants.TokenType.ElseToken, Constants.TokenType.EndCommand])): + + #expect no expression - just <> + parser.expect_symbol([Constants.TokenType.BeginCommand]) + parser.expect_symbol([Constants.TokenType.ElseToken]) + parser.expect_symbol([Constants.TokenType.EndCommand]) + + #parse until hit endif + var clause_else = Clause.new() + var el_statements = []#statement + while not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.EndIf]): + el_statements.append(Statement.new(self, parser)) + + clause_else.statements = el_statements + clauses.append(clause_else) + + #ignore dedent + while parser.next_symbol_is([Constants.TokenType.Dedent]): + parser.expect_symbol([Constants.TokenType.Dedent]) + + #finish + parser.expect_symbol([Constants.TokenType.BeginCommand]) + parser.expect_symbol([Constants.TokenType.EndIf]) + parser.expect_symbol([Constants.TokenType.EndCommand]) + + + func tree_string(indent_level): + var info = [] + var first = true + + for clause in clauses: + if first: + info.append(tab(indent_level, 'if:')) + elif clause.expression!=null: + info.append(tab(indent_level, 'Else If')) + else: + info.append(tab(indent_level, 'Else:')) + + info.append(clause.tree_string(indent_level +1)) + + return info.join('') + + static func can_parse(parser): + return parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) + pass + +class ValueNode extends ParseNode: + var value + + func _init(parent, parser, token = null).(parent, parser): + + var t = token + if t == null : + parser.expect_symbol([Constants.TokenType.Number, + Constants.TokenType.Variable, Constants.TokenType.Str]) + use_token(t) + + #store value depending on type + func use_token(token): + match token.type: + Constants.TokenType.Number: + value = Value.new(float(token.value)) + Constants.TokenType.Str: + value = Value.new(token.value) + Constants.TokenType.FalseToken: + value = Value.new(false) + Constants.TokenType.TrueToken: + value = Value.new(true) + Constants.TokenType.Variable: + value = Value.new(null) + value.type = Constants.ValueType.Variable + value.variable = token.value + Constants.TokenType.NullToken: + value = Value.new(null) + _: + printerr('%s, Invalid token type' % token.name) + + func tree_string(indent_level): + return tab(indent_level, '%s' % value.value()) + +class ExpressionNode extends ParseNode: + var type + var value + var function + var parameters = [] + + func _init(parent, parser, _value, _function = '', _parameters = []).(parent, parser): + if _value != null: + type = Constants.ExpressionType.Value + value = _value + + else: + type = Constants.ExpressionType.FunctionCall + function = _function + parameters = _parameters + + func tree_string(indent_level): + var info = [] + match type: + Constants.ExpressionType.Value: + return value.tree_string(indent_level) + + Constants.ExpressionType.FunctionCall: + info.append(tab(indent_level, 'Func[%s - parameters(%s)]:{'%[function, parameters.size()])) + for param in parameters: + info.append(param.tree_string(indent_level+1)) + info.append(tab(indent_level, '}')) + + return info.join('') + + # Using Djikstra's shunting-yard algorithm to convert stream of expresions into postfix notation, + # & then build a tree of expressions + + # TODO: Rework expression parsing + static func parse(parent, parser): + var rpn = [] + var op_stack = [] + + #track parameters + var func_stack = [] + + var valid_types = [ + Constants.TokenType.Number, + Constants.TokenType.Variable, + Constants.TokenType.Str, + Constants.TokenType.LeftParen, + Constants.TokenType.RightParen, + Constants.TokenType.Identifier, + Constants.TokenType.Comma, + Constants.TokenType.TrueToken, + Constants.TokenType.FalseToken, + Constants.TokenType.NullToken + ] + valid_types += Operator.op_types() + valid_types.invert() + + var last + + print(parser.tokens.slice(0, 6)) + #read expression content + while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types): + print(parser.tokens.front()) + var next = parser.expect_symbol(valid_types) + + if next.type in [ + Constants.TokenType.Variable, + Constants.TokenType.Number, + Constants.TokenType.Str, + Constants.TokenType.FalseToken, + Constants.TokenType.TrueToken, + Constants.TokenType.NullToken + ]: + + # Output primitives + print('adding value "%s" in expression' % next) + print(op_stack.size()) + if func_stack.size() != 0: + op_stack.append(next) + else: + rpn.append(next) + elif next.type == Constants.TokenType.Identifier: + print('adding function') + op_stack.push_back(next) + func_stack.push_back(next) + + #next token is parent - left + next = parser.expect_symbol([Constants.TokenType.LeftParen]) + op_stack.push_back(next) + + elif next.type == Constants.TokenType.Comma: + #resolve sub expression before moving on + while op_stack.back().type != Constants.TokenType.LeftParen: + var p = op_stack.pop_back() + if p == null: + printerr('unbalanced parenthesis %s' % next.name) + break + rpn.append(p) + + + #next token in op_stack left paren + # next parser token not allowed to be right paren or comma + if parser.next_symbol_is([Constants.TokenType.RightParen, + Constants.TokenType.Comma]): + printerr('Expected Expression : %s' % parser.tokens.front().name) + + #find the closest function on stack + #increment parameters + func_stack.back().parameter_count += 1 + + elif Operator.is_op(next.type): + #this is an operator + + #if this is a minus, we need to determine if it is a + #unary minus or a binary minus. + #unary minus looks like this : -1 + #binary minus looks like this 2 - 3 + #thins get complex when we say stuff like: 1 + -1 + #but its easier when we realize that a minus + #is only unary when the last token was a left paren, + #an operator, or its the first token. + + if next.type == Constants.TokenType.Minus: + if last == null \ + or last.type == Constants.TokenType.LeftParen \ + or Operator.is_op(last.type): + #unary minus + next.type = Constants.TokenType.UnaryMinus + + #cannot assign inside expression + # x = a is the same as x == a + if next.type == Constants.TokenType.EqualToOrAssign: + next.type = Constants.TokenType.EqualTo + + #operator precedence + while ExpressionNode.is_apply_precedence(next.type, op_stack, parser): + var op = op_stack.pop_back() + rpn.append(op) + + op_stack.push_back(next) + + elif next.type == Constants.TokenType.LeftParen: + #entered parenthesis sub expression + op_stack.push_back(next) + elif next.type == Constants.TokenType.RightParen: + #leaving sub expression + # resolve order of operations + var parameters = [] + while op_stack.back().type != Constants.TokenType.LeftParen: + parameters.append(op_stack.pop_back()) + + parser.compiler.assert( + op_stack.back() != null, + 'Unbalanced parenthasis #RightParen. Parser.ExpressionNode' + ) + + + rpn.append_array(parameters) + op_stack.pop_back() + # FIXME: Something is going on with parameter counting, fixed for now + # but needs a bigger rework + if op_stack.back().type == Constants.TokenType.Identifier: + #function call + #last token == left paren this == no parameters + #else + #we have more than 1 param + # if last.type != Constants.TokenType.LeftParen: + # func_stack.back().parameter_count += 1 + func_stack.back().parameter_count = parameters.size() + + rpn.append(op_stack.pop_back()) + func_stack.pop_back() + + #record last token used + last = next + + #no more tokens : pop operators to output + while op_stack.size() > 0: + rpn.append(op_stack.pop_back()) + + #if rpn is empty then this is not expression + if rpn.size() == 0: + printerr('Error parsing expression: Expression not found!') + + #build expression tree + var first = rpn.front() + var eval_stack = []#ExpressionNode + + print(rpn) + + while rpn.size() > 0: + var next = rpn.pop_front() + if Operator.is_op(next.type): + #operation + var info = Operator.op_info(next.type) + + if eval_stack.size() < info.arguments: + printerr( + 'Error parsing : Not enough arguments for %s [ got %s expected - was %s]' \ + % [ + Constants.token_type_name(next.type), + eval_stack.size(), + info.arguments + ] + ) + + var function_parameters = [] + for _i in range(info.arguments): + function_parameters.append(eval_stack.pop_back()) + + function_parameters.invert() + + var function_name = get_func_name(next.type) + var expression = ExpressionNode.new(parent, parser, null, function_name, function_parameters) + + eval_stack.append(expression) + + # A function call + elif next.type == Constants.TokenType.Identifier: + var function_name = next.value + prints(function_name, next.parameter_count) + + var function_parameters = [] + for _i in range(next.parameter_count): + function_parameters.append(eval_stack.pop_back()) + + function_parameters.invert() + + var expression = ExpressionNode.new(parent, parser, null, function_name, function_parameters) + + eval_stack.append(expression) + + # A raw value + else: + var raw_value = ValueNode.new(parent, parser, next) + var expression = ExpressionNode.new(parent, parser, raw_value) + eval_stack.append(expression) + + + # NOTE: We should have a single root expression left + # if more then we failed + parser.compiler.assert( + eval_stack.size() == 1, + '[%s] Error parsing expression (stack did not reduce correctly)' % first, + first.line_number, + first.column + ) + + return eval_stack.pop_back() + + static func get_func_name(_type): + var string = '' + + for key in Constants.TokenType.keys(): + if Constants.TokenType[key] == _type: + return key + return string + + static func is_apply_precedence(_type, operator_stack, parser): + if operator_stack.size() == 0: + return false + + if not Operator.is_op(_type): + parser.compiler.assert(false, 'Unable to parse expression!') + return false + + var second = operator_stack.back().type + + if not Operator.is_op(second): + return false + + var first_info = Operator.op_info(_type) + var second_info = Operator.op_info(second) + + return \ + (first_info.associativity == Associativity.Left \ + and first_info.precedence <= second_info.precedence) \ + or \ + (first_info.associativity == Associativity.Right \ + and first_info.precedence < second_info.precedence) + +class Assignment extends ParseNode: + var destination + var value + var operation + + func _init(parent, parser).(parent, parser): + parser.expect_symbol([Constants.TokenType.BeginCommand]) + parser.expect_symbol([Constants.TokenType.Set]) + destination = parser.expect_symbol([Constants.TokenType.Variable]).value + operation = parser.expect_symbol(Assignment.valid_ops()).type + value = ExpressionNode.parse(self, parser) + parser.expect_symbol([Constants.TokenType.EndCommand]) + + func tree_string(indent_level): + var info = [] + info.append(tab(indent_level, 'set:')) + info.append(tab(indent_level + 1, destination)) + info.append(tab(indent_level + 1, Constants.token_type_name(operation))) + info.append(value.tree_string(indent_level + 1)) + return PoolStringArray(info).join('') + + + static func can_parse(parser): + return parser.next_symbols_are([ + Constants.TokenType.BeginCommand, + Constants.TokenType.Set + ]) + + static func valid_ops(): + return [ + Constants.TokenType.EqualToOrAssign, + Constants.TokenType.AddAssign, + Constants.TokenType.MinusAssign, + Constants.TokenType.DivideAssign, + Constants.TokenType.MultiplyAssign + ] + +class Operator extends ParseNode: + var op_type + + func _init(parent, parser, _op_type = null).(parent, parser): + if _op_type == null : + op_type = parser.expect_symbol(Operator.op_types()).type + else: + op_type = _op_type + + func tree_string(indent_level): + var info = [] + info.append(tab(indent_level, op_type)) + return info.join('') + + static func op_info(op): + if not Operator.is_op(op) : + printerr('%s is not a valid operator' % op.name) + + #determine associativity and operands + # each operand has + var TokenType = Constants.TokenType + + match op: + TokenType.Not, TokenType.UnaryMinus: + return OperatorInfo.new(Associativity.Right, 30, 1) + TokenType.Multiply, TokenType.Divide, TokenType.Modulo: + return OperatorInfo.new(Associativity.Left, 20, 2) + TokenType.Add, TokenType.Minus: + return OperatorInfo.new(Associativity.Left, 15, 2) + TokenType.GreaterThan, TokenType.LessThan, TokenType.GreaterThanOrEqualTo, TokenType.LessThanOrEqualTo: + return OperatorInfo.new(Associativity.Left, 10, 2) + TokenType.EqualTo, TokenType.EqualToOrAssign, TokenType.NotEqualTo: + return OperatorInfo.new(Associativity.Left, 5, 2) + TokenType.And: + return OperatorInfo.new(Associativity.Left, 4, 2) + TokenType.Or: + return OperatorInfo.new(Associativity.Left, 3, 2) + TokenType.Xor: + return OperatorInfo.new(Associativity.Left, 2, 2) + _: + printerr('Unknown operator: %s' % op.name) + return null + + static func is_op(type): + return type in op_types() + + static func op_types(): + return [ + Constants.TokenType.Not, + Constants.TokenType.UnaryMinus, + + Constants.TokenType.Add, + Constants.TokenType.Minus, + Constants.TokenType.Divide, + Constants.TokenType.Multiply, + Constants.TokenType.Modulo, + + Constants.TokenType.EqualToOrAssign, + Constants.TokenType.EqualTo, + Constants.TokenType.GreaterThan, + Constants.TokenType.GreaterThanOrEqualTo, + Constants.TokenType.LessThan, + Constants.TokenType.LessThanOrEqualTo, + Constants.TokenType.NotEqualTo, + + Constants.TokenType.And, + Constants.TokenType.Or, + + Constants.TokenType.Xor + ] + +class OperatorInfo: + var associativity + var precedence = -1 + var arguments = -1 + + func _init(_associativity, _precedence, _arguments): + associativity = _associativity + precedence = _precedence + arguments = _arguments + +class Clause: + var expression + var statements = [] #Statement + + func _init(_expression = null, _statements = []): + expression = _expression + statements = _statements + + func tree_string(indent_level): + var info = [] + + if expression != null: + info.append(expression.tree_string(indent_level)) + info.append(tab(indent_level, '{')) + + for statement in statements: + info.append(statement.tree_string(indent_level + 1)) + + info.append(tab(indent_level, '}')) + return PoolStringArray(info).join('') + + func tab(indent_level, input, newline = true): + return '%*s| %s%s' % [indent_level * 2, '', input, '' if !newline else '\n'] diff --git a/dialogue.yarn b/dialogue.wol similarity index 99% rename from dialogue.yarn rename to dialogue.wol index 4faa15e..869e4b6 100644 --- a/dialogue.yarn +++ b/dialogue.wol @@ -1,31 +1,3 @@ -title: Start -tags: -colorID: -position: 0, 0 ---- -<> -<> - -// remove "to" to trigger error -<> -<> - -// Implement inline expressions -<> -Narrator: You, {$direction} way! -<> -Narrator: Do you know you've been here {visit_count()} times? -You: Did you know one + one equals {$one + $one}? -Narrator: You wanna go somewhere? - --> Go to the store - [[TheStore]] --> How much did I visit the store? - Narrator: You've been to the store { visit_count("TheStore") } times. - [[Start]] --> Lets stay here and talk - [[Talk]] -=== title: TheStore tags: colorID: @@ -68,3 +40,31 @@ Narrator: Do you want to continue talking? [[Start]] -> No === +title: Start +tags: +colorID: +position: 0, 0 +--- +<> +<> + +// remove "to" to trigger error +<> +<> + +// Implement inline expressions +<> +Narrator: You, {$direction} way! +<> +Narrator: Do you know you've been here {visit_count()} times? +You: Did you know one + one equals {$one + $one}? +Narrator: You wanna go somewhere? + +-> Go to the store + [[TheStore]] +-> How much did I visit the store? + Narrator: You've been to the store { visit_count("TheStore") } times. + [[Start]] +-> Lets stay here and talk + [[Talk]] +=== \ No newline at end of file diff --git a/project.godot b/project.godot index e6a9e01..e94e828 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=4 [application] config/name="Wol" -run/main_scene="res://Dialogue.tscn" +run/main_scene="res://TestScene.tscn" config/icon="res://icon.png" [debug] From 369b01cd47c68d8c0d878f3e84f025c9c4cc8191 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:50:47 +0100 Subject: [PATCH 08/19] removed uncapitalized ghost files --- addons/Wol/core/constants.gd | 214 ----------------------------------- addons/Wol/core/library.gd | 25 ---- addons/Wol/core/program.gd | 146 ------------------------ addons/Wol/core/value.gd | 137 ---------------------- 4 files changed, 522 deletions(-) delete mode 100644 addons/Wol/core/constants.gd delete mode 100644 addons/Wol/core/library.gd delete mode 100644 addons/Wol/core/program.gd delete mode 100644 addons/Wol/core/value.gd diff --git a/addons/Wol/core/constants.gd b/addons/Wol/core/constants.gd deleted file mode 100644 index c700d85..0000000 --- a/addons/Wol/core/constants.gd +++ /dev/null @@ -1,214 +0,0 @@ -extends Object - -enum ExecutionState { - Stopped, - Running, - WaitingForOption, - Suspended -} - -enum HandlerState { - PauseExecution, - ContinueExecution -} - -#Compile Status return -enum CompileStatus { - Succeeded, SucceededUntaggedStrings, -} - -enum ByteCode { - # opA = string: label name - Label, - # opA = string: label name - JumpTo, - # peek string from stack and jump to that label - Jump, - # opA = int: string number - RunLine, - # opA = string: command text - RunCommand, - # opA = int: string number for option to add - AddOption, - # present the current list of options, then clear the list; most recently - # selected option will be on the top of the stack - ShowOptions, - # opA = int: string number in table; push string to stack - PushString, - # opA = float: number to push to stack - PushNumber, - # opA = int (0 or 1): bool to push to stack - PushBool, - # pushes a null value onto the stack - PushNull, - # opA = string: label name if top of stack is not null, zero or false, jumps - # to that label - JumpIfFalse, - # discard top of stack - Pop, - # opA = string; looks up function, pops as many arguments as needed, result is - # pushed to stack - CallFunc, - # opA = name of variable to get value of and push to stack - PushVariable, - # opA = name of variable to store top of stack in - StoreVariable, - # stops execution - Stop, - # run the node whose name is at the top of the stack - RunNode -} - -enum TokenType { - - #0 Special tokens - Whitespace, Indent, Dedent, EndOfLine, EndOfInput, - - #5 Numbers. Everybody loves a number - Number, - - #6 Strings. Everybody also loves a string - Str, - - #7 '#' - TagMarker, - - #8 Command syntax ('<>') - BeginCommand, EndCommand, - - ExpressionFunctionStart, # { - ExpressionFunctionEnd, # } - - FormatFunctionStart, # [ - FormatFunctionEnd, # ] - - #10 Variables ('$foo') - Variable, - - #11 Shortcut syntax ('->') - ShortcutOption, - - #12 Option syntax ('[[Let's go here|Destination]]') - OptionStart, # [[ - OptionDelimit, # | - OptionEnd, # ]] - - #15 Command types (specially recognised command word) - IfToken, ElseIf, ElseToken, EndIf, Set, - - #20 Boolean values - TrueToken, FalseToken, - - #22 The null value - NullToken, - - #23 Parentheses - LeftParen, RightParen, - - #25 Parameter delimiters - Comma, - - #26 Operators - EqualTo, # ==, eq, is - GreaterThan, # >, gt - GreaterThanOrEqualTo, # >=, gte - LessThan, # <, lt - LessThanOrEqualTo, # <=, lte - NotEqualTo, # !=, neq - - #32 Logical operators - Or, # ||, or - And, # &&, and - Xor, # ^, xor - Not, # !, not - - # this guy's special because '=' can mean either 'equal to' - #36 or 'becomes' depending on context - EqualToOrAssign, # =, to - - #37 - UnaryMinus, # -; this is differentiated from Minus - # when parsing expressions - - #38 - Add, # + - Minus, # - - Multiply, # * - Divide, # / - Modulo, # % - - #43 - AddAssign, # += - MinusAssign, # -= - MultiplyAssign, # *= - DivideAssign, # /= - - Comment, # a run of text that we ignore - - Identifier, # a single word (used for functions) - - Text # a run of text until we hit other syntax -} - -enum ExpressionType { - Value, - FunctionCall -} - -enum StatementTypes { - CustomCommand, - ShortcutOptionGroup, - Block, - IfStatement, - OptionStatement, - AssignmentStatement, - Line, - InlineExpression -} - -enum ValueType { - Number, - Str, - Boolean, - Variable, - Nullean -} - -static func token_type_name(value): - for key in TokenType.keys(): - if TokenType[key] == value: - return key - return 'NOTVALID' - -static func merge_dir(target, patch): - for key in patch: - target[key] = patch[key] - -static func bytecode_name(bytecode): - return [ - 'Label', - 'JumpTo', - 'Jump', - 'RunLine', - 'RunCommand', - 'AddOption', - 'ShowOptions', - 'PushString', - 'PushNumber', - 'PushBool', - 'PushNull', - 'JumpIfFalse', - 'Pop', - 'CallFunc', - 'PushVariable', - 'StoreVariable', - 'Stop', - 'RunNode' - ][bytecode] - -static func token_name(type): - for key in TokenType.keys(): - if TokenType[key] == type: - return key - return '' - diff --git a/addons/Wol/core/library.gd b/addons/Wol/core/library.gd deleted file mode 100644 index a0ff7c5..0000000 --- a/addons/Wol/core/library.gd +++ /dev/null @@ -1,25 +0,0 @@ -extends Object - -const FunctionInfo = preload('res://addons/Wol/core/FunctionInfo.gd') -const Constants = preload('res://addons/Wol/core/Constants.gd') - -var functions = {} -var virtual_machine - -func get_function(name): - if functions.has(name): - return functions[name] - else : - printerr('Invalid Function: %s'% name) - return - -func import_library(other): - Constants.merge_dir(functions, other.functions) - other.virtual_machine = virtual_machine - -func register_function(name, parameter_count, function, returns_value): - var functionInfo = FunctionInfo.new(name, parameter_count, function, returns_value) - functions[name] = functionInfo - -func deregister_function(name): - functions.erase(name) diff --git a/addons/Wol/core/program.gd b/addons/Wol/core/program.gd deleted file mode 100644 index 4b9d282..0000000 --- a/addons/Wol/core/program.gd +++ /dev/null @@ -1,146 +0,0 @@ -extends Object - -const Constants = preload('res://addons/Wol/core/Constants.gd') - -var name = '' -var filename = '' -var strings = {} -var nodes = {} - -class Line: - var text = '' - var node_name = '' - var line_number = -1 - var file_name = '' - var implicit = false - var substitutions = [] - var meta = [] - - func _init(_text, _node_name, _line_number, _file_name, _implicit, _meta): - text = _text - node_name = _node_name - file_name = _file_name - implicit = _implicit - meta = _meta - - func clone(): - return get_script().new(text, node_name, line_number, file_name, implicit, meta) - - func _to_string(): - return '%s:%d: "%s"' % [file_name.get_file(), line_number, text] - -class Option: - var line - var id = -1 - var destination = '' - - func _init(_line, _id, _destination): - line = _line - id = _id - destination = _destination - - func clone(): - return get_script().new(self) - -class Command: - var command = '' - - func _init(_command): - command = _command - -class WolNode: - var name = '' - var instructions = [] - var labels = {} - var tags = [] - var source_id = '' - - func _init(other = null): - if other != null and other.get_script() == self.get_script(): - name = other.name - instructions += other.instructions - for key in other.labels.keys(): - labels[key] = other.labels[key] - tags += other.tags - source_id = other.source_id - - func equals(other): - if other.get_script() != get_script(): - return false - if other.name != name: - return false - if other.instructions != instructions: - return false - if other.labels != labels: - return false - if other.source_id != source_id: - return false - return true - - func _to_string(): - return "WolNode[%s:%s]" % [name, source_id] - -# TODO: Make this make sense -class Operand: - enum ValueType { - None, - StringValue, - BooleanValue, - FloatValue - } - - var value - var type - - func _init(_value): - if typeof(_value) == TYPE_OBJECT and _value.get_script() == get_script(): - set_value(_value.value) - else: - set_value(_value) - - func set_value(_value): - match typeof(_value): - TYPE_REAL,TYPE_INT: - set_number(_value) - TYPE_BOOL: - set_boolean(_value) - TYPE_STRING: - set_string(_value) - - func set_boolean(_value): - value = _value - type = ValueType.BooleanValue - return self - - func set_string(_value): - value = _value - type = ValueType.StringValue - return self - - func set_number(_value): - value = _value - type = ValueType.FloatValue - return self - - func clear_value(): - type = ValueType.None - value = null - - func clone(): - return get_script().new(self) - - func _to_string(): - return "Operand[%s:%s]" % [type, value] - -class Instruction: - var operation = -1 - var operands = [] - - func _init(other = null): - if other != null and other.get_script() == self.get_script(): - self.operation = other.operation - self.operands += other.operands - - func _to_string(): - return Constants.bytecode_name(operation) + ':' + operands as String - diff --git a/addons/Wol/core/value.gd b/addons/Wol/core/value.gd deleted file mode 100644 index 161df5a..0000000 --- a/addons/Wol/core/value.gd +++ /dev/null @@ -1,137 +0,0 @@ -extends Object - -const Constants = preload('res://addons/Wol/core/Constants.gd') - -const NANI = 'NaN' - -var type = Constants.ValueType.Nullean -var number = 0 -var string = '' -var variable = '' -var boolean = false - -func _init(value = NANI): - if typeof(value) == TYPE_OBJECT and value.get_script() == get_script(): - if value.type == Constants.ValueType.Variable: - type = value.type - variable = value.variable - else: - set_value(value) - -func value(): - match type: - Constants.ValueType.Number: - return number - Constants.ValueType.Str: - return string - Constants.ValueType.Boolean: - return boolean - Constants.ValueType.Variable: - return variable - return null - -func as_bool(): - match type: - Constants.ValueType.Number: - return number != 0 - Constants.ValueType.Str: - return !string.empty() - Constants.ValueType.Boolean: - return boolean - return false - -func as_string(): - return '%s' % value() - -func as_number(): - match type: - Constants.ValueType.Number: - return number - Constants.ValueType.Str: - return float(string) - Constants.ValueType.Boolean: - return 0.0 if !boolean else 1.0 - return .0 - -func set_value(value): - if value == null or (typeof(value) == TYPE_STRING and value == NANI): - type = Constants.ValueType.Nullean - return - - match typeof(value): - TYPE_INT, TYPE_REAL: - type = Constants.ValueType.Number - number = value - TYPE_STRING: - type = Constants.ValueType.Str - string = value - TYPE_BOOL: - type = Constants.ValueType.Boolean - boolean = value - -func add(other): - if type == Constants.ValueType.Str or other.type == Constants.ValueType.Str: - return get_script().new('%s%s' % [value(), other.value()]) - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return get_script().new(number + other.number) - return null - -func equals(other): - if other.get_script() != get_script(): - return false - if other.value() != value(): - return false - # TODO: Add more equality cases - return true - -func sub(other): - if type == Constants.ValueType.Str or other.type == Constants.ValueType.Str: - return get_script().new(str(value()).replace(str(other.value()),'')) - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return get_script().new(number - other.number) - return null - -func mult(other): - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return get_script().new(number * other.number) - return null - -func div(other): - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return get_script().new(number / other.number) - return null - -func mod(other): - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return get_script().new(number % other.number) - return null - -func negative(): - if type == Constants.ValueType.Number: - return get_script().new(-number) - return null - -func greater(other): - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return number > other.number - return false - -func less(other): - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return number < other.number - return false - -func geq(other): - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return number > other.number or equals(other) - return false - -func leq(other): - if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number: - return number < other.number or equals(other) - return false - -func _to_string(): - return 'value(type[%s]: %s)' % [type,value()] - - From ca63c6810dcadd7a99d07557619952c950d9485e Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:54:13 +0100 Subject: [PATCH 09/19] Removed more ghost files (on mac os) --- addons/Wol/core/compiler/compiler.gd | 461 ----------- addons/Wol/core/compiler/lexer.gd | 485 ------------ addons/Wol/core/compiler/parser.gd | 1065 -------------------------- 3 files changed, 2011 deletions(-) delete mode 100644 addons/Wol/core/compiler/compiler.gd delete mode 100644 addons/Wol/core/compiler/lexer.gd delete mode 100644 addons/Wol/core/compiler/parser.gd diff --git a/addons/Wol/core/compiler/compiler.gd b/addons/Wol/core/compiler/compiler.gd deleted file mode 100644 index 617042d..0000000 --- a/addons/Wol/core/compiler/compiler.gd +++ /dev/null @@ -1,461 +0,0 @@ -extends Object - -signal error(message, line_number, column) - -const Constants = preload('res://addons/Wol/core/Constants.gd') -const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') -const Program = preload('res://addons/Wol/core/Program.gd') -const Parser = preload('res://addons/Wol/core/compiler/Parser.gd') - -const INVALID_TITLE = '[\\[<>\\]{}\\|:\\s#\\$]' - -var source = '' -var filename = '' - -var current_node -var has_implicit_string_tags = false -var soft_assert = false - -var string_count = 0 -var string_table = {} -var label_count = 0 - -func _init(_filename, _source = null, _soft_assert = false): - filename = _filename - soft_assert = _soft_assert - - if not _filename and _source: - self.source = _source - else: - var file = File.new() - file.open(_filename, File.READ) - self.source = file.get_as_text() - file.close() - - var source_lines = source.split('\n') - for i in range(source_lines.size()): - source_lines[i] = source_lines[i].strip_edges(false, true) - - source = source_lines.join('\n') - -func get_headers(offset = 0): - var header_property = RegEx.new() - var header_sep = RegEx.new() - - header_sep.compile('---(\r\n|\r|\n)') - header_property.compile('(?.*): *(?.*)') - - self.assert(header_sep.search(source), 'No headers found!') - - var title = '' - var position = Vector2.ZERO - - var source_lines = source.split('\n') - var line_number = offset - while line_number < source_lines.size(): - var line = source_lines[line_number] - line_number += 1 - - if not line.empty(): - var result = header_property.search(line) - - if result != null: - var field = result.get_string('field') - var value = result.get_string('value') - - if field == 'title': - var regex = RegEx.new() - regex.compile(INVALID_TITLE) - self.assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)]) - - title = value - - if field == 'position': - var regex = RegEx.new() - regex.compile('^position:.*,.*\\d$') - self.assert(regex.search(line), 'Couldn\'t parse position property in the headers, got "%s" instead in node "%s"' % [value, title]) - - position = Vector2(int(value.split(',')[0].strip_edges()), int(value.split(',')[1].strip_edges())) - - # TODO: Implement color and tags - - if line == '---': - break - - return { - 'title': title, - 'position': position - } - -func get_body(offset = 0): - var body_lines = [] - - var source_lines = source.split('\n') - var recording = false - var line_number = offset - - while line_number < source_lines.size() and source_lines[line_number] != '===': - if recording: - body_lines.append(source_lines[line_number]) - - recording = recording or source_lines[line_number] == '---' - line_number += 1 - - line_number += 1 - - return PoolStringArray(body_lines).join('\n') - -func get_nodes(): - var nodes = [] - var line_number = 0 - var source_lines = source.split('\n') - while line_number < source_lines.size(): - var headers = get_headers(line_number) - var body = get_body(line_number) - headers.body = body - - nodes.append(headers) - - # Add +2 to the final line to skip the === from that node - line_number = Array(source_lines).find_last(body.split('\n')[-1]) + 2 - - while line_number < source_lines.size() and source_lines[line_number].empty(): - line_number += 1 - - return nodes - -func assert(statement, message, line_number = -1, column = -1, _absolute_line_number = -1): - if not soft_assert: - assert(statement, message + ('; on line %d column %d' % [line_number, column])) - elif not statement: - emit_signal('error', message, line_number, column) - -func compile(): - var parsed_nodes = [] - for node in get_nodes(): - var lexer = Lexer.new(self, filename, node.title, node.body) - var tokens = lexer.tokenize() - - # In case of lexer error - if not tokens: - return - - var parser = Parser.new(self, node.title, tokens) - var parser_node = parser.parse_node() - - parser_node.name = node.title - parsed_nodes.append(parser_node) - - var program = Program.new() - program.filename = filename - - for node in parsed_nodes: - compile_node(program, node) - - for key in string_table: - program.strings[key] = string_table[key] - - return program - -func compile_node(program, parsed_node): - self.assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name) - - var node_compiled = Program.WolNode.new() - - node_compiled.name = parsed_node.name - node_compiled.tags = parsed_node.tags - - if parsed_node.source != null and not parsed_node.source.empty(): - node_compiled.source_id = register_string( - parsed_node.source, - parsed_node.name, - 'line:' + parsed_node.name, - 0, - [] - ) - else: - var start_label = register_label() - emit(Constants.ByteCode.Label, node_compiled, [Program.Operand.new(start_label)]) - - for statement in parsed_node.statements: - generate_statement(node_compiled, statement) - - var dangling_options = false - for instruction in node_compiled.instructions: - if instruction.operation == Constants.ByteCode.AddOption: - dangling_options = true - if instruction.operation == Constants.ByteCode.ShowOptions: - dangling_options = false - - if dangling_options: - emit(Constants.ByteCode.ShowOptions, node_compiled) - emit(Constants.ByteCode.RunNode, node_compiled) - else: - emit(Constants.ByteCode.Stop, node_compiled) - - program.nodes[node_compiled.name] = node_compiled - -func register_string(text, node_name, id = '', line_number = -1, tags = []): - var line_id_used = '' - var implicit = false - - if id.empty(): - line_id_used = '%s-%s-%d' % [filename, node_name, string_count] - string_count += 1 - - #use this when we generate implicit tags - #they are not saved and are generated - #aka dummy tags that change on each compilation - has_implicit_string_tags = true - - implicit = true - else : - line_id_used = id - implicit = false - - var string_info = Program.Line.new(text, node_name, line_number, filename, implicit, tags) - string_table[line_id_used] = string_info - - return line_id_used - -func register_label(comment = ''): - label_count += 1 - return 'Label%s%s' % [label_count, comment] - -func emit(bytecode, node = current_node, operands = []): - var instruction = Program.Instruction.new(null) - instruction.operation = bytecode - instruction.operands = operands - - if node == null: - printerr('Trying to emit to null node with byte_code: %s' % bytecode) - return - - node.instructions.append(instruction) - - if bytecode == Constants.ByteCode.Label: - node.labels[instruction.operands[0].value] = node.instructions.size() - 1 - -func generate_statement(node, statement): - match statement.type: - Constants.StatementTypes.CustomCommand: - generate_custom_command(node, statement.custom_command) - - Constants.StatementTypes.ShortcutOptionGroup: - generate_shortcut_group(node, statement.shortcut_option_group) - - Constants.StatementTypes.Block: - generate_block(node, statement.block.statements) - - Constants.StatementTypes.IfStatement: - generate_if(node, statement.if_statement) - - Constants.StatementTypes.OptionStatement: - generate_option(node, statement.option_statement) - - Constants.StatementTypes.AssignmentStatement: - generate_assignment(node, statement.assignment) - - Constants.StatementTypes.Line: - generate_line(node, statement) - _: - self.assert(false, statement.line_number, 'Illegal statement type [%s]. Could not generate code.' % statement.type) - -func generate_custom_command(node, command): - # TODO: See if the first tree of this statement is being used - if command.expression != null: - generate_expression(node, command.expression) - else: - var command_string = command.client_command - if command_string == 'stop': - emit(Constants.ByteCode.Stop, node) - else : - emit(Constants.ByteCode.RunCommand, node, [Program.Operand.new(command_string)]) - -func generate_line(node, statement): - # TODO: Implement proper line numbers (global and local) - var line = statement.line - var expression_count = line.substitutions.size() - - while not line.substitutions.empty(): - var inline_expression = line.substitutions.pop_back() - generate_expression(node, inline_expression.expression) - - var num = register_string(line.line_text, node.name, line.line_id, statement.line_number, line.tags); - emit(Constants.ByteCode.RunLine, node,[Program.Operand.new(num), Program.Operand.new(expression_count)]) - -func generate_shortcut_group(node, shortcut_group): - var end = register_label('group_end') - var labels = [] - var option_count = 0 - - for option in shortcut_group.options: - var endof_clause = '' - var op_destination = register_label('option_%s' % [option_count + 1]) - - labels.append(op_destination) - - if option.condition != null: - endof_clause = register_label('conditional_%s' % option_count) - generate_expression(node, option.condition) - emit(Constants.ByteCode.JumpIfFalse, node, [Program.Operand.new(endof_clause)]) - - var label_line_id = '' #TODO: Add tag support - var label_string_id = register_string( - option.label, - node.name, - label_line_id, - option.line_number, - [] - ) - - emit(Constants.ByteCode.AddOption, node, [Program.Operand.new(label_string_id), Program.Operand.new(op_destination)]) - - if option.condition != null: - emit(Constants.ByteCode.Label, node, [Program.Operand.new(endof_clause)]) - emit(Constants.ByteCode.Pop, node) - - option_count += 1 - - emit(Constants.ByteCode.ShowOptions, node) - emit(Constants.ByteCode.Jump, node) - - option_count = 0 - - for option in shortcut_group.options: - emit(Constants.ByteCode.Label, node, [Program.Operand.new(labels[option_count])]) - - if option.node != null: - generate_block(node, option.node.statements) - emit(Constants.ByteCode.JumpTo, node, [Program.Operand.new(end)]) - option_count += 1 - - emit(Constants.ByteCode.Label, node, [Program.Operand.new(end)]) - emit(Constants.ByteCode.Pop, node) - -func generate_block(node, statements = []): - if not statements.empty(): - for statement in statements: - generate_statement(node, statement) - - -func generate_if(node, if_statement): - var endif = register_label('endif') - - for clause in if_statement.clauses: - var end_clause = register_label('skip_clause') - - if clause.expression != null: - generate_expression(node, clause.expression) - emit(Constants.ByteCode.JumpIfFalse, node, [Program.Operand.new(end_clause)]) - - generate_block(node, clause.statements) - emit(Constants.ByteCode.JumpTo, node, [Program.Operand.new(endif)]) - - if clause.expression != null: - emit(Constants.ByteCode.Label, node, [Program.Operand.new(end_clause)]) - - if clause.expression != null: - emit(Constants.ByteCode.Pop) - - emit(Constants.ByteCode.Label, node, [Program.Operand.new(endif)]) - -func generate_option(node, option): - var destination = option.destination - - if option.label == null or option.label.empty(): - emit(Constants.ByteCode.RunNode, node, [Program.Operand.new(destination)]) - else : - var line_id = '' #TODO: ADD TAG SUPPORT - var string_id = register_string(option.label, node.name, line_id, option.line_number, []) - - emit(Constants.ByteCode.AddOption, node, [Program.Operand.new(string_id), Program.Operand.new(destination)]) - -func generate_assignment(node, assignment): - if assignment.operation == Constants.TokenType.EqualToOrAssign: - generate_expression(node, assignment.value) - else : - emit(Constants.ByteCode.PushVariable, node, [assignment.destination]) - generate_expression(node, assignment.value) - - match assignment.operation: - Constants.TokenType.AddAssign: - emit( - Constants.ByteCode.CallFunc, - node, - [Program.Operand.new(Constants.token_type_name(Constants.TokenType.Add))] - ) - Constants.TokenType.MinusAssign: - emit( - Constants.ByteCode.CallFunc, - node, - [Program.Operand.new(Constants.token_type_name(Constants.TokenType.Minus))] - ) - Constants.TokenType.MultiplyAssign: - emit( - Constants.ByteCode.CallFunc, - node, - [Program.Operand.new(Constants.token_type_name(Constants.TokenType.MultiplyAssign))] - ) - Constants.TokenType.DivideAssign: - emit( - Constants.ByteCode.CallFunc, - node, - [Program.Operand.new(Constants.token_type_name(Constants.TokenType.DivideAssign))] - ) - _: - printerr('Unable to generate assignment') - - emit(Constants.ByteCode.StoreVariable, node, [Program.Operand.new(assignment.destination)]) - emit(Constants.ByteCode.Pop, node) - -func generate_expression(node, expression): - match expression.type: - Constants.ExpressionType.Value: - generate_value(node, expression.value) - Constants.ExpressionType.FunctionCall: - for parameter in expression.parameters: - generate_expression(node, parameter) - - emit(Constants.ByteCode.PushNumber, node, [Program.Operand.new(expression.parameters.size())]) - emit(Constants.ByteCode.CallFunc, node, [Program.Operand.new(expression.function)]) - _: - printerr('No expression.') - -func generate_value(node, value): - match value.value.type: - Constants.ValueType.Number: - emit( - Constants.ByteCode.PushNumber, - node, - [Program.Operand.new(value.value.as_number())] - ) - Constants.ValueType.Str: - var id = register_string( - value.value.as_string(), - node.name, - '', - value.line_number, - [] - ) - emit( - Constants.ByteCode.PushString, - node, - [Program.Operand.new(id)] - ) - Constants.ValueType.Boolean: - emit( - Constants.ByteCode.PushBool, - node, - [Program.Operand.new(value.value.as_bool())] - ) - Constants.ValueType.Variable: - emit( - Constants.ByteCode.PushVariable, - node, - [Program.Operand.new(value.value.variable)] - ) - Constants.ValueType.Nullean: - emit(Constants.ByteCode.PushNull, node) - _: - printerr('Unrecognized valuenode type: %s' % value.value.type) diff --git a/addons/Wol/core/compiler/lexer.gd b/addons/Wol/core/compiler/lexer.gd deleted file mode 100644 index b651b99..0000000 --- a/addons/Wol/core/compiler/lexer.gd +++ /dev/null @@ -1,485 +0,0 @@ -extends Object - -const Constants = preload('res://addons/Wol/core/Constants.gd') - -const LINE_COMMENT = '//' -const FORWARD_SLASH = '/' -const LINE_SEPARATOR = '\n' - -const BASE = 'base' -const DASH = '-' -const COMMAND = 'command' -const LINK = 'link' -const SHORTCUT = 'shortcut' -const TAG = 'tag' -const EXPRESSION = 'expression' -const ASSIGNMENT = 'assignment' -const OPTION = 'option' -const OR = 'or' -const DESTINATION = 'destination' -const INLINE = 'inline' -const FORMAT_FUNCTION = 'format' - -var WHITESPACE = '\\s*' - -var compiler -var filename = '' -var title = '' -var text = '' - -var states = {} -var default_state -var current_state - -var indent_stack = [] -var should_track_indent = false - -func _init(_compiler, _filename, _title, _text): - create_states() - - compiler = _compiler - filename = _filename - title = _title - text = _text - -func create_states(): - var patterns = {} - patterns[Constants.TokenType.Text] = ['.*', 'any text'] - - patterns[Constants.TokenType.Number] = ['\\-?[0-9]+(\\.[0-9+])?', 'any number'] - patterns[Constants.TokenType.Str] = ['\"([^\"\\\\]*(?:\\.[^\"\\\\]*)*)\"', 'any text'] - patterns[Constants.TokenType.TagMarker] = ['\\#', 'a tag #'] - patterns[Constants.TokenType.LeftParen] = ['\\(', 'left parenthesis ('] - patterns[Constants.TokenType.RightParen] = ['\\)', 'right parenthesis )'] - patterns[Constants.TokenType.EqualTo] = ['(==|is(?!\\w)|eq(?!\\w))', '"=", "is" or "eq"'] - patterns[Constants.TokenType.EqualToOrAssign] = ['(=|to(?!\\w))', '"=" or "to"'] - patterns[Constants.TokenType.NotEqualTo] = ['(\\!=|neq(?!\\w))', '"!=" or "neq"'] - patterns[Constants.TokenType.GreaterThanOrEqualTo] = ['(\\>=|gte(?!\\w))', '">=" or "gte"'] - patterns[Constants.TokenType.GreaterThan] = ['(\\>|gt(?!\\w))', '">" or "gt"'] - patterns[Constants.TokenType.LessThanOrEqualTo] = ['(\\<=|lte(?!\\w))', '"<=" or "lte"'] - patterns[Constants.TokenType.LessThan] = ['(\\<|lt(?!\\w))', '"<" or "lt"'] - patterns[Constants.TokenType.AddAssign] = ['\\+=', '"+="'] - patterns[Constants.TokenType.MinusAssign] = ['\\-=', '"-="'] - patterns[Constants.TokenType.MultiplyAssign] = ['\\*=', '"*="'] - patterns[Constants.TokenType.DivideAssign] = ['\\/=', '"/="'] - patterns[Constants.TokenType.Add] = ['\\+', '"+"'] - patterns[Constants.TokenType.Minus] = ['\\-', '"-"'] - patterns[Constants.TokenType.Multiply] = ['\\*', '"*"'] - patterns[Constants.TokenType.Divide] = ['\\/', '"/"'] - patterns[Constants.TokenType.Modulo] = ['\\%', '"%"'] - patterns[Constants.TokenType.And] = ['(\\&\\&|and(?!\\w))', '"&&" or "and"'] - patterns[Constants.TokenType.Or] = ['(\\|\\||or(?!\\w))', '"||" or "or"'] - patterns[Constants.TokenType.Xor] = ['(\\^|xor(?!\\w))', '"^" or "xor"'] - patterns[Constants.TokenType.Not] = ['(\\!|not(?!\\w))', '"!" or "not"'] - patterns[Constants.TokenType.Variable] = ['\\$([A-Za-z0-9_\\.])+', 'any variable'] - patterns[Constants.TokenType.Comma] = ['\\,', '","'] - patterns[Constants.TokenType.TrueToken] = ['true(?!\\w)', '"true"'] - patterns[Constants.TokenType.FalseToken] = ['false(?!\\w)', '"false"'] - patterns[Constants.TokenType.NullToken] = ['null(?!\\w)', '"null"'] - patterns[Constants.TokenType.BeginCommand] = ['\\<\\<', 'beginning of a command "<<"'] - patterns[Constants.TokenType.EndCommand] = ['\\>\\>', 'ending of a command ">>"'] - patterns[Constants.TokenType.OptionStart] = ['\\[\\[', 'start of an option "[["'] - patterns[Constants.TokenType.OptionEnd] = ['\\]\\]', 'end of an option "]]"'] - patterns[Constants.TokenType.OptionDelimit] = ['\\|', 'middle of an option "|"'] - patterns[Constants.TokenType.Identifier] = ['[a-zA-Z0-9_:\\.]+', 'any reference to another node'] - patterns[Constants.TokenType.IfToken] = ['if(?!\\w)', '"if"'] - patterns[Constants.TokenType.ElseToken] = ['else(?!\\w)', '"else"'] - patterns[Constants.TokenType.ElseIf] = ['elseif(?!\\w)', '"elseif"'] - patterns[Constants.TokenType.EndIf] = ['endif(?!\\w)', '"endif"'] - patterns[Constants.TokenType.Set] = ['set(?!\\w)', '"set"'] - patterns[Constants.TokenType.ShortcutOption] = ['\\-\\>\\s*', '"->"'] - patterns[Constants.TokenType.ExpressionFunctionStart] = ['\\{', '"{"'] - patterns[Constants.TokenType.ExpressionFunctionEnd] = ['\\}', '"}"'] - patterns[Constants.TokenType.FormatFunctionStart] = ['(? previous_indentation: - indent_stack.push_front([indentation, true]) - - var indent = Token.new( - Constants.TokenType.Indent, - current_state, - filename, - line_number, - previous_indentation - ) - indent.value = '%*s' % [indentation - previous_indentation, ''] - - should_track_indent = false - token_stack.push_front(indent) - - elif indentation < previous_indentation: - while indentation < indent_stack.front()[0]: - var top = indent_stack.pop_front()[1] - if top: - var deindent = Token.new(Constants.TokenType.Dedent, current_state, line_number, 0) - token_stack.push_front(deindent) - - var column = indentation - var whitespace = RegEx.new() - whitespace.compile(WHITESPACE) - - while column < fresh_line.length(): - if fresh_line.substr(column).begins_with(LINE_COMMENT): - break - - var matched = false - - for rule in current_state.rules: - var found = rule.regex.search(fresh_line, column) - - if !found: - continue - - var token_text = '' - - # NOTE: If this is text then we back up to the most recent delimiting token - # and treat everything from there as text. - if rule.token_type == Constants.TokenType.Text: - - var start_index = indentation - - if token_stack.size() > 0 : - while token_stack.front().type == Constants.TokenType.Identifier: - token_stack.pop_front() - - var start_delimit_token = token_stack.front() - start_index = start_delimit_token.column - - if start_delimit_token.type == Constants.TokenType.Indent: - start_index += start_delimit_token.value.length() - if start_delimit_token.type == Constants.TokenType.Dedent: - start_index = indentation - - column = start_index - var end_index = found.get_start() + found.get_string().length() - - token_text = fresh_line.substr(start_index, end_index - start_index) - else: - token_text = found.get_string() - - column += token_text.length() - - if rule.token_type == Constants.TokenType.Str: - token_text = token_text.substr(1, token_text.length() - 2) - token_text = token_text.replace('\\\\', '\\') - token_text = token_text.replace('\\\'','\'') - - var token = Token.new( - rule.token_type, - current_state, - filename, - line_number, - column, - token_text - ) - token.delimits_text = rule.delimits_text - - token_stack.push_front(token) - - if rule.enter_state != null and rule.enter_state.length() > 0: - if not states.has(rule.enter_state): - printerr('State[%s] not known - line(%s) col(%s)' % [rule.enter_state, line_number, column]) - return [] - - enter_state(states[rule.enter_state]) - - if should_track_indent: - if indent_stack.front()[0] < indentation: - indent_stack.append([indentation, false]) - - matched = true - break - - if not matched: - var rules = [] - for rule in current_state.rules: - rules.append('"%s" (%s)' % [Constants.token_type_name(rule.token_type), rule.human_readable_identifier]) - - var error_data = [ - PoolStringArray(rules).join(', ') if rules.size() == 1 else PoolStringArray(rules.slice(0, rules.size() - 2)).join(', ') + ' or %s' % rules[-1], - filename, - title, - line_number, - column - ] - compiler.assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data, line_number, column) - return - - var last_whitespace = whitespace.search(line, column) - if last_whitespace: - column += last_whitespace.get_string().length() - - token_stack.invert() - - return token_stack - -func line_indentation(line): - var indent_regex = RegEx.new() - indent_regex.compile('^(\\s*)') - - var found = indent_regex.search(line) - - if !found or found.get_string().length() <= 0: - return 0 - - return found.get_string().length() - -func enter_state(state): - current_state = state; - if current_state.track_indent: - should_track_indent = true - -class Token: - var type = -1 - var value = '' - - var filename = '' - var line_number = -1 - var column = -1 - var text = '' - - var delimits_text = false - var parameter_count = -1 - var lexer_state = '' - - func _init(_type, _state, _filename, _line_number = -1, _column = -1, _value = ''): - type = _type - lexer_state = _state.name - filename = _filename - line_number = _line_number - column = _column - value = _value - - func _to_string(): - return '%s (%s) at %s:%s (state: %s)' % [Constants.token_type_name(type), value, line_number, column, lexer_state] - -class LexerState: - var name = '' - var patterns = {} - var rules = [] - var track_indent = false - - func _init(_patterns): - patterns = _patterns - - func add_transition(type, state = '', delimit_text = false): - var pattern = '\\G%s' % patterns[type][0] - var rule = Rule.new(type, pattern, patterns[type][1], state, delimit_text) - rules.append(rule) - return rule - - func add_text_rule(type, state = ''): - if contains_text_rule() : - printerr('State already contains Text rule') - return null - - var delimiters:Array = [] - for rule in rules: - if rule.delimits_text: - delimiters.append('%s' % rule.regex.get_pattern().substr(2)) - - var pattern = '\\G((?!%s).)*' % [PoolStringArray(delimiters).join('|')] - var rule = add_transition(type, state) - rule.regex = RegEx.new() - rule.regex.compile(pattern) - rule.is_text_rule = true - return rule - - func contains_text_rule(): - for rule in rules: - if rule.is_text_rule: - return true - return false - -class Rule: - var regex - - var enter_state = '' - var token_type = -1 - var is_text_rule = false - var delimits_text = false - var human_readable_identifier = '' - - func _init(_type, _regex, _human_readable_identifier, _enter_state, _delimits_text): - token_type = _type - - regex = RegEx.new() - regex.compile(_regex) - - human_readable_identifier = _human_readable_identifier - enter_state = _enter_state - delimits_text = _delimits_text - - func _to_string(): - return '[Rule : %s (%s) - %s]' % [Constants.token_type_name(token_type), human_readable_identifier, regex] diff --git a/addons/Wol/core/compiler/parser.gd b/addons/Wol/core/compiler/parser.gd deleted file mode 100644 index b4385eb..0000000 --- a/addons/Wol/core/compiler/parser.gd +++ /dev/null @@ -1,1065 +0,0 @@ -extends Object - -# warnings-disable - -const Constants = preload('res://addons/Wol/core/Constants.gd') -const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd') -const Value = preload('res://addons/Wol/core/Value.gd') - -var compiler -var title = '' -var tokens = [] - -func _init(_compiler, _title, _tokens): - compiler = _compiler - title = _title - tokens = _tokens - -enum Associativity { - Left, - Right, - None -} - -func parse_node(): - return WolNode.new('Start', null, self) - -func next_symbol_is(valid_types): - return tokens.front().type in valid_types - -# NOTE: 0 look ahead for `<<` and `else` -func next_symbols_are(valid_types): - var temporary = [] + tokens - for type in valid_types: - if temporary.pop_front().type != type: - return false - return true - -func expect_symbol(token_types = []): - var token = tokens.pop_front() as Lexer.Token - - if token_types.size() == 0: - if token.type == Constants.TokenType.EndOfInput: - compiler.assert(false, 'Unexpected end of input') - return token - - for type in token_types: - if token.type == type: - return token - - var token_names = [] - for type in token_types: - token_names.append(Constants.token_type_name(type)) - - var error_guess = '\n' - - if Constants.token_type_name(token.type) == 'Identifier' \ - and Constants.token_type_name(token_types[0]) == 'OptionEnd': - error_guess += 'Does the node your refer to have a space in it?' - else: - error_guess = '' - - var error_data = [ - PoolStringArray(token_names).join(', '), - Constants.token_type_name(token.type), - error_guess - ] - compiler.assert(false, 'Expected token "%s" but got "%s"%s' % error_data, token.line_number, token.column) - return token - -static func tab(indent_level, input, newline = true): - return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n'] - -class ParseNode: - var name = '' - - var parent - var line_number = -1 - var tags = [] - - func _init(_parent, _parser): - parent = _parent - - var tokens = _parser.tokens as Array - if tokens.size() > 0: - line_number = tokens.front().line_number - - tags = [] - - func tree_string(_indent_level): - return 'Not_implemented' - - func tags_to_string(_indent_level): - return 'TAGSNOTIMPLEMENTED' - - func get_node_parent(): - var node = self - while node != null: - if node is ParseNode: - return node as WolNode - node = node.parent - return null - - func tab(indent_level, input, newline = true): - return '%*s| %s%s' % [ indent_level * 2, '', input, '' if !newline else '\n'] - -class WolNode extends ParseNode: - var source = '' - - var editor_node_tags = [] - var statements = [] - var parser - - func _init(_name, parent, _parser).(parent, _parser): - name = _name - parser = _parser - while parser.tokens.size() > 0 \ - and not parser.next_symbol_is([Constants.TokenType.Dedent, Constants.TokenType.EndOfInput]): - - parser.compiler.assert( - not parser.next_symbol_is([Constants.TokenType.Indent]), - 'Found a stray indentation!', - parser.tokens.front().line_number, - parser.tokens.front().column - ) - - statements.append(Statement.new(self, parser)) - - func tree_string(indent_level): - var info = [] - for statement in statements: - info.append(statement.tree_string(indent_level + 1)) - - return PoolStringArray(info).join('') - -class Statement extends ParseNode: - var Type = Constants.StatementTypes - - var type = -1 - var block - var if_statement - var option_statement - var assignment - var shortcut_option_group - var custom_command - var line - - func _init(parent, parser).(parent, parser): - if Block.can_parse(parser): - block = Block.new(self, parser) - type = Type.Block - - elif IfStatement.can_parse(parser): - if_statement = IfStatement.new(self, parser) - type = Type.IfStatement - - elif OptionStatement.can_parse(parser): - option_statement = OptionStatement.new(self, parser) - type = Type.OptionStatement - - elif Assignment.can_parse(parser): - assignment = Assignment.new(self, parser) - type = Type.AssignmentStatement - - elif ShortcutOptionGroup.can_parse(parser): - shortcut_option_group = ShortcutOptionGroup.new(self, parser) - type = Type.ShortcutOptionGroup - - elif CustomCommand.can_parse(parser): - custom_command = CustomCommand.new(self, parser) - type = Type.CustomCommand - - elif parser.next_symbol_is([Constants.TokenType.Text]): - line = LineNode.new(self, parser) - type = Type.Line - - else: - parser.compiler.assert(false, 'Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string()) - - var tags = [] - - while parser.next_symbol_is([Constants.TokenType.TagMarker]): - parser.expect_symbol([Constants.TokenType.TagMarker]) - var tag = parser.expect_symbol([Constants.TokenType.Identifier]).value - tags.append(tag) - - if tags.size() > 0: - self.tags = tags - - func tree_string(indent_level): - var info = [] - - match type: - Type.Block: - info.append(block.tree_string(indent_level)) - Type.IfStatement: - info.append(if_statement.tree_string(indent_level)) - Type.AssignmentStatement: - info.append(assignment.tree_string(indent_level)) - Type.OptionStatement: - info.append(option_statement.tree_string(indent_level)) - Type.ShortcutOptionGroup: - info.append(shortcut_option_group.tree_string(indent_level)) - Type.CustomCommand: - info.append(custom_command.tree_string(indent_level)) - Type.Line: - info.append(line.tree_string(indent_level)) - _: - printerr('Cannot print statement') - - return PoolStringArray(info).join('') - -class InlineExpression extends ParseNode: - var expression - - func _init(parent, parser).(parent, parser): - parser.expect_symbol([Constants.TokenType.ExpressionFunctionStart]) - expression = ExpressionNode.parse(self, parser) - parser.expect_symbol([Constants.TokenType.ExpressionFunctionEnd]) - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.ExpressionFunctionStart]) - - func tree_string(_indent_level): - return "InlineExpression:" - -# Returns a format_text string as [ name "{0}" key1="value1" key2="value2" ] -class FormatFunctionNode extends ParseNode: - var format_text = '' - var expression_value - - func _init(parent:ParseNode, parser, expressionCount:int).(parent, parser): - format_text="[" - parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) - - # FIXME: Add exit condition in case of failure - while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): - if parser.next_symbol_is([Constants.TokenType.Text]): - format_text += parser.expect_symbol().value - - if InlineExpression.can_parse(parser): - expression_value = InlineExpression.new(self, parser) - format_text +=" \"{%d}\" " % expressionCount - parser.expect_symbol() - format_text+="]" - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.FormatFunctionStart]) - - # TODO: Make format prettier and add more information - func tree_string(_indent_level): - return "FormatFunction" - -class LineNode extends ParseNode: - var line_text = '' - # FIXME: Right now we are putting the formatfunctions and inline expressions in the same - # list but if at some point we want to strongly type our sub list we need to make a new - # parse node that can have either an InlineExpression or a FunctionFormat - # .. This is a consideration for Godot4.x - var substitutions = [] - var line_id = '' - var line_tags = [] - - # NOTE: If format function an inline functions are both present - # returns a line in the format "Some text {0} and some other {1}[format "{2}" key="value" key="value"]" - - func _init(parent, parser).(parent, parser): - while parser.next_symbol_is( - [ - Constants.TokenType.FormatFunctionStart, - Constants.TokenType.ExpressionFunctionStart, - Constants.TokenType.Text, - Constants.TokenType.TagMarker - ] - ): - - if FormatFunctionNode.can_parse(parser): - var format_function = FormatFunctionNode.new(self, parser, substitutions.size()) - if format_function.expression_value != null: - substitutions.append(format_function.expression_value) - - line_text += format_function.format_text - - elif InlineExpression.can_parse(parser): - var inline_expression = InlineExpression.new(self, parser) - line_text += '{%d}' % substitutions.size() - substitutions.append(inline_expression) - - elif parser.next_symbols_are([Constants.TokenType.TagMarker, Constants.TokenType.Identifier]): - parser.expect_symbol() - var tag_token = parser.expect_symbol([ Constants.TokenType.Identifier ]) - if tag_token.value.begins_with("line:"): - if line_id.empty(): - line_id = tag_token.value - else: - printerr("Too many line_tags @[%s:%d]" % [parser.currentNodeName, tag_token.line_number]) - return - else: - tags.append(tag_token.value) - - else: - var token = parser.expect_symbol() - if token.line_number == line_number and token.type != Constants.TokenType.BeginCommand: - line_text += token.value - else: - parser.tokens.push_front(token) - break - - - func tree_string(indent_level): - return tab(indent_level, 'Line: (%s)[%d]' % [line_text, substitutions.size()]) - - -class CustomCommand extends ParseNode: - - enum Type { - Expression, - ClientCommand - } - - var type = -1 - var expression - var client_command - - func _init(parent, parser).(parent, parser): - parser.expect_symbol([Constants.TokenType.BeginCommand]) - - var command_tokens = [] - command_tokens.append(parser.expect_symbol()) - - # FIXME: add exit condition - while not parser.next_symbol_is([Constants.TokenType.EndCommand]): - command_tokens.append(parser.expect_symbol()) - - parser.expect_symbol([Constants.TokenType.EndCommand]) - - #if first token is identifier and second is leftt parenthesis - #evaluate as function - if command_tokens.size() > 1 \ - and command_tokens[0].type == Constants.TokenType.Identifier \ - and command_tokens[1].type == Constants.TokenType.LeftParen: - - var p = get_script().new(command_tokens, parser.library) - expression = ExpressionNode.parse(self, p) - type = Type.Expression - - else: - # otherwise evaluate command - type = Type.ClientCommand - client_command = command_tokens[0].value - - func tree_string(indent_level): - match type: - Type.Expression: - return tab(indent_level, 'Expression: %s' % expression.tree_string(indent_level+1)) - Type.ClientCommand: - return tab(indent_level, 'Command: %s' % client_command) - return '' - - static func can_parse(parser): - return (parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.Text]) - or parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.Identifier])) - -class ShortcutOptionGroup extends ParseNode: - var options = [] - - func _init(parent, parser).(parent, parser): - - # parse options until there is no more - # expect one otherwise invalid - - var index = 0 - while parser.next_symbol_is([Constants.TokenType.ShortcutOption]): - options.append(ShortCutOption.new(index, self, parser)) - index += 1 - - func tree_string(indent_level): - var info = [] - - info.append(tab(indent_level, 'Shortcut Option Group{')) - - for option in options: - info.append(option.tree_string(indent_level+1)) - - info.append(tab(indent_level, '}')) - - return PoolStringArray(info).join('') - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.ShortcutOption]) - -class ShortCutOption extends ParseNode: - var label = '' - var condition - var node - - func _init(index, parent, parser).(parent, parser): - parser.expect_symbol([Constants.TokenType.ShortcutOption]) - label = parser.expect_symbol([Constants.TokenType.Text]).value - - # FIXME: Parse the conditional << if $x >> when it exists - var tags = [] - while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) \ - or parser.next_symbol_is([Constants.TokenType.TagMarker]): - - if parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]): - parser.expect_symbol([Constants.TokenType.BeginCommand]) - parser.expect_symbol([Constants.TokenType.IfToken]) - condition = ExpressionNode.parse(self, parser) - parser.expect_symbol([Constants.TokenType.EndCommand]) - - elif parser.next_symbol_is([Constants.TokenType.TagMarker]): - parser.expect_symbol([Constants.TokenType.TagMarker]) - var tag = parser.expect_symbol([Constants.TokenType.Identifier]).value - tags.append(tag) - - - self.tags = tags - # parse remaining statements - - if parser.next_symbol_is([Constants.TokenType.Indent]): - parser.expect_symbol([Constants.TokenType.Indent]) - node = WolNode.new('%s.%s' % [parent.name, index], self, parser) - parser.expect_symbol([Constants.TokenType.Dedent]) - - - func tree_string(indent_level): - var info = [] - - info.append(tab(indent_level, 'Option \'%s\'' % label)) - - if condition != null: - info.append(tab(indent_level + 1, '(when:')) - info.append(condition.tree_string(indent_level + 2)) - info.append(tab(indent_level + 1, '),')) - - if node != null: - info.append(tab(indent_level, '{')) - info.append(node.tree_string(indent_level + 1)) - info.append(tab(indent_level, '}')) - - return PoolStringArray(info).join('') - -#Blocks are groups of statements with the same indent level -class Block extends ParseNode: - - var statements = [] - - func _init(parent, parser).(parent, parser): - #read indent - parser.expect_symbol([Constants.TokenType.Indent]) - - #keep reading statements until we hit a dedent - # FIXME: find exit condition - while not parser.next_symbol_is([Constants.TokenType.Dedent]): - #parse all statements including nested blocks - statements.append(Statement.new(self, parser)) - - #clean up dedent - parser.expect_symbol([Constants.TokenType.Dedent]) - - - func tree_string(indent_level): - var info = [] - - info.append(tab(indent_level, 'Block {')) - - for statement in statements: - info.append(statement.tree_string(indent_level + 1)) - - info.append(tab(indent_level, '}')) - - return PoolStringArray(info).join('') - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.Indent]) - -# NOTE: Option Statements are links to other nodes -class OptionStatement extends ParseNode: - var destination = '' - var label = '' - - func _init(parent, parser).(parent, parser): - var strings = [] - - # NOTE: parse [[LABEL - parser.expect_symbol([Constants.TokenType.OptionStart]) - strings.append(parser.expect_symbol([Constants.TokenType.Text]).value) - - # NOTE: if there is a | get the next string - if parser.next_symbol_is([Constants.TokenType.OptionDelimit]): - parser.expect_symbol([Constants.TokenType.OptionDelimit]) - var t = parser.expect_symbol([Constants.TokenType.Text, Constants.TokenType.Identifier]) - - strings.append(t.value as String) - - label = strings[0] if strings.size() > 1 else '' - destination = strings[1] if strings.size() > 1 else strings[0] - - parser.expect_symbol([Constants.TokenType.OptionEnd]) - - func tree_string(indent_level): - if label != null: - return tab(indent_level, 'Option: %s -> %s' % [label, destination]) - else: - return tab(indent_level, 'Option: -> %s' % destination) - - static func can_parse(parser): - return parser.next_symbol_is([Constants.TokenType.OptionStart]) - -class IfStatement extends ParseNode: - var clauses = []# - - func _init(parent, parser).(parent, parser): - - #<> - var prime = Clause.new() - - parser.expect_symbol([Constants.TokenType.BeginCommand]) - parser.expect_symbol([Constants.TokenType.IfToken]) - prime.expression = ExpressionNode.parse(self, parser) - parser.expect_symbol([Constants.TokenType.EndCommand]) - - #read statements until 'endif' or 'else' or 'else if' - var statements = []#statement - while not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.EndIf]) \ - and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseToken]) \ - and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseIf]): - - statements.append(Statement.new(self, parser)) - - #ignore dedent - while parser.next_symbol_is([Constants.TokenType.Dedent]): - parser.expect_symbol([Constants.TokenType.Dedent]) - - prime.statements = statements - clauses.append(prime) - - #handle all else if - while parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseIf]): - var clause_elif = Clause.new() - - #parse condition syntax - parser.expect_symbol([Constants.TokenType.BeginCommand]) - parser.expect_symbol([Constants.TokenType.ElseIf]) - clause_elif.expression = ExpressionNode.parse(self, parser) - parser.expect_symbol([Constants.TokenType.EndCommand]) - - - var elif_statements = []#statement - while not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.EndIf]) \ - and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseToken]) \ - and not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.ElseIf]): - - elif_statements.append(Statement.new(self, parser)) - - #ignore dedent - while parser.next_symbol_is([Constants.TokenType.Dedent]): - parser.expect_symbol([Constants.TokenType.Dedent]) - - clause_elif.statements = statements - clauses.append(clause_elif) - - #handle else if exists - if (parser.next_symbols_are([Constants.TokenType.BeginCommand, - Constants.TokenType.ElseToken, Constants.TokenType.EndCommand])): - - #expect no expression - just <> - parser.expect_symbol([Constants.TokenType.BeginCommand]) - parser.expect_symbol([Constants.TokenType.ElseToken]) - parser.expect_symbol([Constants.TokenType.EndCommand]) - - #parse until hit endif - var clause_else = Clause.new() - var el_statements = []#statement - while not parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.EndIf]): - el_statements.append(Statement.new(self, parser)) - - clause_else.statements = el_statements - clauses.append(clause_else) - - #ignore dedent - while parser.next_symbol_is([Constants.TokenType.Dedent]): - parser.expect_symbol([Constants.TokenType.Dedent]) - - #finish - parser.expect_symbol([Constants.TokenType.BeginCommand]) - parser.expect_symbol([Constants.TokenType.EndIf]) - parser.expect_symbol([Constants.TokenType.EndCommand]) - - - func tree_string(indent_level): - var info = [] - var first = true - - for clause in clauses: - if first: - info.append(tab(indent_level, 'if:')) - elif clause.expression!=null: - info.append(tab(indent_level, 'Else If')) - else: - info.append(tab(indent_level, 'Else:')) - - info.append(clause.tree_string(indent_level +1)) - - return info.join('') - - static func can_parse(parser): - return parser.next_symbols_are([Constants.TokenType.BeginCommand, Constants.TokenType.IfToken]) - pass - -class ValueNode extends ParseNode: - var value - - func _init(parent, parser, token = null).(parent, parser): - - var t = token - if t == null : - parser.expect_symbol([Constants.TokenType.Number, - Constants.TokenType.Variable, Constants.TokenType.Str]) - use_token(t) - - #store value depending on type - func use_token(token): - match token.type: - Constants.TokenType.Number: - value = Value.new(float(token.value)) - Constants.TokenType.Str: - value = Value.new(token.value) - Constants.TokenType.FalseToken: - value = Value.new(false) - Constants.TokenType.TrueToken: - value = Value.new(true) - Constants.TokenType.Variable: - value = Value.new(null) - value.type = Constants.ValueType.Variable - value.variable = token.value - Constants.TokenType.NullToken: - value = Value.new(null) - _: - printerr('%s, Invalid token type' % token.name) - - func tree_string(indent_level): - return tab(indent_level, '%s' % value.value()) - -class ExpressionNode extends ParseNode: - var type - var value - var function - var parameters = [] - - func _init(parent, parser, _value, _function = '', _parameters = []).(parent, parser): - if _value != null: - type = Constants.ExpressionType.Value - value = _value - - else: - type = Constants.ExpressionType.FunctionCall - function = _function - parameters = _parameters - - func tree_string(indent_level): - var info = [] - match type: - Constants.ExpressionType.Value: - return value.tree_string(indent_level) - - Constants.ExpressionType.FunctionCall: - info.append(tab(indent_level, 'Func[%s - parameters(%s)]:{'%[function, parameters.size()])) - for param in parameters: - info.append(param.tree_string(indent_level+1)) - info.append(tab(indent_level, '}')) - - return info.join('') - - # Using Djikstra's shunting-yard algorithm to convert stream of expresions into postfix notation, - # & then build a tree of expressions - - # TODO: Rework expression parsing - static func parse(parent, parser): - var rpn = [] - var op_stack = [] - - #track parameters - var func_stack = [] - - var valid_types = [ - Constants.TokenType.Number, - Constants.TokenType.Variable, - Constants.TokenType.Str, - Constants.TokenType.LeftParen, - Constants.TokenType.RightParen, - Constants.TokenType.Identifier, - Constants.TokenType.Comma, - Constants.TokenType.TrueToken, - Constants.TokenType.FalseToken, - Constants.TokenType.NullToken - ] - valid_types += Operator.op_types() - valid_types.invert() - - var last - - print(parser.tokens.slice(0, 6)) - #read expression content - while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types): - print(parser.tokens.front()) - var next = parser.expect_symbol(valid_types) - - if next.type in [ - Constants.TokenType.Variable, - Constants.TokenType.Number, - Constants.TokenType.Str, - Constants.TokenType.FalseToken, - Constants.TokenType.TrueToken, - Constants.TokenType.NullToken - ]: - - # Output primitives - print('adding value "%s" in expression' % next) - print(op_stack.size()) - if func_stack.size() != 0: - op_stack.append(next) - else: - rpn.append(next) - elif next.type == Constants.TokenType.Identifier: - print('adding function') - op_stack.push_back(next) - func_stack.push_back(next) - - #next token is parent - left - next = parser.expect_symbol([Constants.TokenType.LeftParen]) - op_stack.push_back(next) - - elif next.type == Constants.TokenType.Comma: - #resolve sub expression before moving on - while op_stack.back().type != Constants.TokenType.LeftParen: - var p = op_stack.pop_back() - if p == null: - printerr('unbalanced parenthesis %s' % next.name) - break - rpn.append(p) - - - #next token in op_stack left paren - # next parser token not allowed to be right paren or comma - if parser.next_symbol_is([Constants.TokenType.RightParen, - Constants.TokenType.Comma]): - printerr('Expected Expression : %s' % parser.tokens.front().name) - - #find the closest function on stack - #increment parameters - func_stack.back().parameter_count += 1 - - elif Operator.is_op(next.type): - #this is an operator - - #if this is a minus, we need to determine if it is a - #unary minus or a binary minus. - #unary minus looks like this : -1 - #binary minus looks like this 2 - 3 - #thins get complex when we say stuff like: 1 + -1 - #but its easier when we realize that a minus - #is only unary when the last token was a left paren, - #an operator, or its the first token. - - if next.type == Constants.TokenType.Minus: - if last == null \ - or last.type == Constants.TokenType.LeftParen \ - or Operator.is_op(last.type): - #unary minus - next.type = Constants.TokenType.UnaryMinus - - #cannot assign inside expression - # x = a is the same as x == a - if next.type == Constants.TokenType.EqualToOrAssign: - next.type = Constants.TokenType.EqualTo - - #operator precedence - while ExpressionNode.is_apply_precedence(next.type, op_stack, parser): - var op = op_stack.pop_back() - rpn.append(op) - - op_stack.push_back(next) - - elif next.type == Constants.TokenType.LeftParen: - #entered parenthesis sub expression - op_stack.push_back(next) - elif next.type == Constants.TokenType.RightParen: - #leaving sub expression - # resolve order of operations - var parameters = [] - while op_stack.back().type != Constants.TokenType.LeftParen: - parameters.append(op_stack.pop_back()) - - parser.compiler.assert( - op_stack.back() != null, - 'Unbalanced parenthasis #RightParen. Parser.ExpressionNode' - ) - - - rpn.append_array(parameters) - op_stack.pop_back() - # FIXME: Something is going on with parameter counting, fixed for now - # but needs a bigger rework - if op_stack.back().type == Constants.TokenType.Identifier: - #function call - #last token == left paren this == no parameters - #else - #we have more than 1 param - # if last.type != Constants.TokenType.LeftParen: - # func_stack.back().parameter_count += 1 - func_stack.back().parameter_count = parameters.size() - - rpn.append(op_stack.pop_back()) - func_stack.pop_back() - - #record last token used - last = next - - #no more tokens : pop operators to output - while op_stack.size() > 0: - rpn.append(op_stack.pop_back()) - - #if rpn is empty then this is not expression - if rpn.size() == 0: - printerr('Error parsing expression: Expression not found!') - - #build expression tree - var first = rpn.front() - var eval_stack = []#ExpressionNode - - print(rpn) - - while rpn.size() > 0: - var next = rpn.pop_front() - if Operator.is_op(next.type): - #operation - var info = Operator.op_info(next.type) - - if eval_stack.size() < info.arguments: - printerr( - 'Error parsing : Not enough arguments for %s [ got %s expected - was %s]' \ - % [ - Constants.token_type_name(next.type), - eval_stack.size(), - info.arguments - ] - ) - - var function_parameters = [] - for _i in range(info.arguments): - function_parameters.append(eval_stack.pop_back()) - - function_parameters.invert() - - var function_name = get_func_name(next.type) - var expression = ExpressionNode.new(parent, parser, null, function_name, function_parameters) - - eval_stack.append(expression) - - # A function call - elif next.type == Constants.TokenType.Identifier: - var function_name = next.value - prints(function_name, next.parameter_count) - - var function_parameters = [] - for _i in range(next.parameter_count): - function_parameters.append(eval_stack.pop_back()) - - function_parameters.invert() - - var expression = ExpressionNode.new(parent, parser, null, function_name, function_parameters) - - eval_stack.append(expression) - - # A raw value - else: - var raw_value = ValueNode.new(parent, parser, next) - var expression = ExpressionNode.new(parent, parser, raw_value) - eval_stack.append(expression) - - - # NOTE: We should have a single root expression left - # if more then we failed - parser.compiler.assert( - eval_stack.size() == 1, - '[%s] Error parsing expression (stack did not reduce correctly)' % first, - first.line_number, - first.column - ) - - return eval_stack.pop_back() - - static func get_func_name(_type): - var string = '' - - for key in Constants.TokenType.keys(): - if Constants.TokenType[key] == _type: - return key - return string - - static func is_apply_precedence(_type, operator_stack, parser): - if operator_stack.size() == 0: - return false - - if not Operator.is_op(_type): - parser.compiler.assert(false, 'Unable to parse expression!') - return false - - var second = operator_stack.back().type - - if not Operator.is_op(second): - return false - - var first_info = Operator.op_info(_type) - var second_info = Operator.op_info(second) - - return \ - (first_info.associativity == Associativity.Left \ - and first_info.precedence <= second_info.precedence) \ - or \ - (first_info.associativity == Associativity.Right \ - and first_info.precedence < second_info.precedence) - -class Assignment extends ParseNode: - var destination - var value - var operation - - func _init(parent, parser).(parent, parser): - parser.expect_symbol([Constants.TokenType.BeginCommand]) - parser.expect_symbol([Constants.TokenType.Set]) - destination = parser.expect_symbol([Constants.TokenType.Variable]).value - operation = parser.expect_symbol(Assignment.valid_ops()).type - value = ExpressionNode.parse(self, parser) - parser.expect_symbol([Constants.TokenType.EndCommand]) - - func tree_string(indent_level): - var info = [] - info.append(tab(indent_level, 'set:')) - info.append(tab(indent_level + 1, destination)) - info.append(tab(indent_level + 1, Constants.token_type_name(operation))) - info.append(value.tree_string(indent_level + 1)) - return PoolStringArray(info).join('') - - - static func can_parse(parser): - return parser.next_symbols_are([ - Constants.TokenType.BeginCommand, - Constants.TokenType.Set - ]) - - static func valid_ops(): - return [ - Constants.TokenType.EqualToOrAssign, - Constants.TokenType.AddAssign, - Constants.TokenType.MinusAssign, - Constants.TokenType.DivideAssign, - Constants.TokenType.MultiplyAssign - ] - -class Operator extends ParseNode: - var op_type - - func _init(parent, parser, _op_type = null).(parent, parser): - if _op_type == null : - op_type = parser.expect_symbol(Operator.op_types()).type - else: - op_type = _op_type - - func tree_string(indent_level): - var info = [] - info.append(tab(indent_level, op_type)) - return info.join('') - - static func op_info(op): - if not Operator.is_op(op) : - printerr('%s is not a valid operator' % op.name) - - #determine associativity and operands - # each operand has - var TokenType = Constants.TokenType - - match op: - TokenType.Not, TokenType.UnaryMinus: - return OperatorInfo.new(Associativity.Right, 30, 1) - TokenType.Multiply, TokenType.Divide, TokenType.Modulo: - return OperatorInfo.new(Associativity.Left, 20, 2) - TokenType.Add, TokenType.Minus: - return OperatorInfo.new(Associativity.Left, 15, 2) - TokenType.GreaterThan, TokenType.LessThan, TokenType.GreaterThanOrEqualTo, TokenType.LessThanOrEqualTo: - return OperatorInfo.new(Associativity.Left, 10, 2) - TokenType.EqualTo, TokenType.EqualToOrAssign, TokenType.NotEqualTo: - return OperatorInfo.new(Associativity.Left, 5, 2) - TokenType.And: - return OperatorInfo.new(Associativity.Left, 4, 2) - TokenType.Or: - return OperatorInfo.new(Associativity.Left, 3, 2) - TokenType.Xor: - return OperatorInfo.new(Associativity.Left, 2, 2) - _: - printerr('Unknown operator: %s' % op.name) - return null - - static func is_op(type): - return type in op_types() - - static func op_types(): - return [ - Constants.TokenType.Not, - Constants.TokenType.UnaryMinus, - - Constants.TokenType.Add, - Constants.TokenType.Minus, - Constants.TokenType.Divide, - Constants.TokenType.Multiply, - Constants.TokenType.Modulo, - - Constants.TokenType.EqualToOrAssign, - Constants.TokenType.EqualTo, - Constants.TokenType.GreaterThan, - Constants.TokenType.GreaterThanOrEqualTo, - Constants.TokenType.LessThan, - Constants.TokenType.LessThanOrEqualTo, - Constants.TokenType.NotEqualTo, - - Constants.TokenType.And, - Constants.TokenType.Or, - - Constants.TokenType.Xor - ] - -class OperatorInfo: - var associativity - var precedence = -1 - var arguments = -1 - - func _init(_associativity, _precedence, _arguments): - associativity = _associativity - precedence = _precedence - arguments = _arguments - -class Clause: - var expression - var statements = [] #Statement - - func _init(_expression = null, _statements = []): - expression = _expression - statements = _statements - - func tree_string(indent_level): - var info = [] - - if expression != null: - info.append(expression.tree_string(indent_level)) - info.append(tab(indent_level, '{')) - - for statement in statements: - info.append(statement.tree_string(indent_level + 1)) - - info.append(tab(indent_level, '}')) - return PoolStringArray(info).join('') - - func tab(indent_level, input, newline = true): - return '%*s| %s%s' % [indent_level * 2, '', input, '' if !newline else '\n'] From 51c899eca6c4d1790413fb36ffa19e0c160efa6d Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 17:55:26 +0100 Subject: [PATCH 10/19] removed some prints --- addons/Wol/core/compiler/Parser.gd | 8 -------- 1 file changed, 8 deletions(-) diff --git a/addons/Wol/core/compiler/Parser.gd b/addons/Wol/core/compiler/Parser.gd index b4385eb..b227af1 100644 --- a/addons/Wol/core/compiler/Parser.gd +++ b/addons/Wol/core/compiler/Parser.gd @@ -700,10 +700,8 @@ class ExpressionNode extends ParseNode: var last - print(parser.tokens.slice(0, 6)) #read expression content while parser.tokens.size() > 0 and parser.next_symbol_is(valid_types): - print(parser.tokens.front()) var next = parser.expect_symbol(valid_types) if next.type in [ @@ -716,14 +714,11 @@ class ExpressionNode extends ParseNode: ]: # Output primitives - print('adding value "%s" in expression' % next) - print(op_stack.size()) if func_stack.size() != 0: op_stack.append(next) else: rpn.append(next) elif next.type == Constants.TokenType.Identifier: - print('adding function') op_stack.push_back(next) func_stack.push_back(next) @@ -829,8 +824,6 @@ class ExpressionNode extends ParseNode: var first = rpn.front() var eval_stack = []#ExpressionNode - print(rpn) - while rpn.size() > 0: var next = rpn.pop_front() if Operator.is_op(next.type): @@ -861,7 +854,6 @@ class ExpressionNode extends ParseNode: # A function call elif next.type == Constants.TokenType.Identifier: var function_name = next.value - prints(function_name, next.parameter_count) var function_parameters = [] for _i in range(next.parameter_count): From 31447732adf75717c6d79d0a0c98cc0015e68fb8 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 18:01:01 +0100 Subject: [PATCH 11/19] added icon for editor --- addons/Wol/plugin.gd | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/Wol/plugin.gd b/addons/Wol/plugin.gd index ed5a6cb..df46bba 100644 --- a/addons/Wol/plugin.gd +++ b/addons/Wol/plugin.gd @@ -35,5 +35,9 @@ func get_plugin_name(): return 'Wol' func get_plugin_icon(): - print(get_editor_interface().get_base_control().get_icon('Node', 'EditorIcons')) - return get_editor_interface().get_base_control().get_icon('Node', 'EditorIcons') + var icon = ImageTexture.new() + var image = Image.new() + image.load('res://addons/Wol/icon-white.svg') + image.resize(34, 34) + icon.create_from_image(image) + return icon From a35752129e81ada7ef21527bd33f9bbfb1ee7307 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 19:43:04 +0100 Subject: [PATCH 12/19] moved the button in the main editor --- README.md | 6 +++++- addons/Wol/core/compiler/Compiler.gd | 2 +- addons/Wol/editor/WolEditor.gd | 7 ++----- addons/Wol/editor/WolEditor.tscn | 4 ++-- addons/Wol/editor/WolGraphNode.gd | 19 ++++++++++++++++--- addons/Wol/plugin.gd | 13 +++++++++++++ 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a4419fd..6aabd33 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,12 @@ There are few things that need to be ironed out to be 100% feature compatible wi - [ ] Integration with Godot's translation/localization system. - [ ] Auto generation for `#line:` suffixes - [ ] Support for [format functions](https://yarnspinner.dev/docs/syntax/#format-functions). -- [ ] Support for conditional options. +- [ ] ~Support~ Fix for conditional options. - [ ] In-editor dialogue editor with preview. + - [ ] Lines connecting different nodes if they refer to eachother. + - [x] Error hints when doing something wrong. + - [x] Basic saving, opening and saving-as. +- [ ] Remove all `printerr` in favor of (soft) `assert`s. - [x] Fully extend the documentation of this project. - [x] Porting to usable signals in Godot. - [x] Providing helpful errors when failing to compile. diff --git a/addons/Wol/core/compiler/Compiler.gd b/addons/Wol/core/compiler/Compiler.gd index 617042d..55a4a62 100644 --- a/addons/Wol/core/compiler/Compiler.gd +++ b/addons/Wol/core/compiler/Compiler.gd @@ -126,7 +126,7 @@ func get_nodes(): func assert(statement, message, line_number = -1, column = -1, _absolute_line_number = -1): if not soft_assert: - assert(statement, message + ('; on line %d column %d' % [line_number, column])) + assert(statement, '"%s" on line %d column %d' % [message, line_number, column]) elif not statement: emit_signal('error', message, line_number, column) diff --git a/addons/Wol/editor/WolEditor.gd b/addons/Wol/editor/WolEditor.gd index 1677b7c..08a2c66 100644 --- a/addons/Wol/editor/WolEditor.gd +++ b/addons/Wol/editor/WolEditor.gd @@ -5,7 +5,6 @@ const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') onready var GraphNodeTemplate = $GraphNodeTemplate var path -var compiler func _ready(): for menu_button in [$Menu/File]: @@ -13,13 +12,11 @@ func _ready(): # TODO: Conditionally load in theme based on Editor or standalone - path = 'res://dialogue.yarn' + path = 'res://dialogue.wol' build_nodes() func build_nodes(): - compiler = Compiler.new(path) - - for node in compiler.get_nodes(): + for node in Compiler.new(path).get_nodes(): var graph_node = GraphNodeTemplate.duplicate() $GraphEdit.add_child(graph_node) graph_node.node = node diff --git a/addons/Wol/editor/WolEditor.tscn b/addons/Wol/editor/WolEditor.tscn index 6fb5aff..e9eac17 100644 --- a/addons/Wol/editor/WolEditor.tscn +++ b/addons/Wol/editor/WolEditor.tscn @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2c2ef9c83d968c8cc99aa62da40477e6f3660aa71f8621e360818cc0307bf0b -size 604323 +oid sha256:8967c8b3e310a5ab4a5c78cf3d4f3b3a94fbcd4e02ed5c736c413e4106cf9249 +size 604382 diff --git a/addons/Wol/editor/WolGraphNode.gd b/addons/Wol/editor/WolGraphNode.gd index dc584cf..1ef08c3 100644 --- a/addons/Wol/editor/WolGraphNode.gd +++ b/addons/Wol/editor/WolGraphNode.gd @@ -1,10 +1,15 @@ tool extends GraphNode +signal recompiled + const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') var node setget set_node +var error_lines = [] +var compiler + onready var text_edit = $TextEdit func _ready(): @@ -12,23 +17,30 @@ func _ready(): text_edit.connect('text_changed', self, '_on_text_changed') $TextDebounce.connect('timeout', self, '_on_debounce') +func get_connections(): + print(compiler) + func _on_text_changed(): $TextDebounce.start(.3) func _on_debounce(): text_edit.get_node('ErrorGutter').hide() node.body = text_edit.text + for line in error_lines: + text_edit.set_line_as_safe(line - 1, false) compile() func _on_offset_changed(): node.position = offset -func _on_error(message, _line_number, _column): +func _on_error(message, line_number, _column): var error_gutter = text_edit.get_node('ErrorGutter') error_gutter.show() error_gutter.text = message - # TODO: Highlight line based on line number and column + error_lines.append(line_number) + + text_edit.set_line_as_safe(line_number - 1, true) func set_node(_node): node = _node @@ -41,6 +53,7 @@ func set_node(_node): func compile(): var text = '---\n%s\n===' % text_edit.text - var compiler = Compiler.new(null, text, true) + compiler = Compiler.new(null, text, true) compiler.connect('error', self, '_on_error') compiler.compile() + emit_signal('recompiled') diff --git a/addons/Wol/plugin.gd b/addons/Wol/plugin.gd index df46bba..9e95aa0 100644 --- a/addons/Wol/plugin.gd +++ b/addons/Wol/plugin.gd @@ -17,6 +17,18 @@ func _enter_tree(): get_editor_interface().get_editor_viewport().add_child(wol_editor_instance) make_visible(false) + + call_deferred('move_button') + +func move_button(): + var buttons = get_editor_interface().get_base_control() + var path = [0, 0, 2] + for child_number in path: + if buttons.get_child_count() > child_number: + buttons = buttons.get_child(child_number) + + if buttons.has_node('AssetLib'): + buttons.get_node('AssetLib').raise() func make_visible(visible): if wol_editor_instance: @@ -35,6 +47,7 @@ func get_plugin_name(): return 'Wol' func get_plugin_icon(): + # FIXME: Change this code so it doesn't show a warning on activation var icon = ImageTexture.new() var image = Image.new() image.load('res://addons/Wol/icon-white.svg') From 588a0bcbec621ff1f74211b48a0415ce3022198e Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 21:14:30 +0100 Subject: [PATCH 13/19] Made connections visible --- addons/Wol/core/compiler/Parser.gd | 9 ++++---- addons/Wol/editor/WolEditor.gd | 32 +++++++++++++++++++++++++++- addons/Wol/editor/WolGraphNode.gd | 34 ++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/addons/Wol/core/compiler/Parser.gd b/addons/Wol/core/compiler/Parser.gd index b227af1..4beacbd 100644 --- a/addons/Wol/core/compiler/Parser.gd +++ b/addons/Wol/core/compiler/Parser.gd @@ -25,7 +25,8 @@ func parse_node(): return WolNode.new('Start', null, self) func next_symbol_is(valid_types): - return tokens.front().type in valid_types + compiler.assert(tokens.size() != 0, 'Ran out of tokens!') + return tokens.front() and tokens.front().type in valid_types # NOTE: 0 look ahead for `<<` and `else` func next_symbols_are(valid_types): @@ -233,7 +234,7 @@ class FormatFunctionNode extends ParseNode: parser.expect_symbol([Constants.TokenType.FormatFunctionStart]) # FIXME: Add exit condition in case of failure - while not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): + while parser.tokens.size() > 0 and not parser.next_symbol_is([Constants.TokenType.FormatFunctionEnd]): if parser.next_symbol_is([Constants.TokenType.Text]): format_text += parser.expect_symbol().value @@ -328,7 +329,7 @@ class CustomCommand extends ParseNode: command_tokens.append(parser.expect_symbol()) # FIXME: add exit condition - while not parser.next_symbol_is([Constants.TokenType.EndCommand]): + while parser.tokens.size() > 0 and not parser.next_symbol_is([Constants.TokenType.EndCommand]): command_tokens.append(parser.expect_symbol()) parser.expect_symbol([Constants.TokenType.EndCommand]) @@ -451,7 +452,7 @@ class Block extends ParseNode: #keep reading statements until we hit a dedent # FIXME: find exit condition - while not parser.next_symbol_is([Constants.TokenType.Dedent]): + while parser.tokens.size() > 0 and not parser.next_symbol_is([Constants.TokenType.Dedent]): #parse all statements including nested blocks statements.append(Statement.new(self, parser)) diff --git a/addons/Wol/editor/WolEditor.gd b/addons/Wol/editor/WolEditor.gd index 08a2c66..0479984 100644 --- a/addons/Wol/editor/WolEditor.gd +++ b/addons/Wol/editor/WolEditor.gd @@ -5,6 +5,7 @@ const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') onready var GraphNodeTemplate = $GraphNodeTemplate var path +var refreshed = false func _ready(): for menu_button in [$Menu/File]: @@ -19,9 +20,12 @@ func build_nodes(): for node in Compiler.new(path).get_nodes(): var graph_node = GraphNodeTemplate.duplicate() $GraphEdit.add_child(graph_node) + + graph_node.connect('recompiled', self, '_on_graph_node_recompiled', [graph_node]) + graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node, node]) + graph_node.node = node graph_node.show() - graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node, node]) func serialize_to_file(): var buffer = [] @@ -93,6 +97,32 @@ func _on_menu_pressed(index, node): 'Open': open() +# FIXME: Come up with better way of showing connections between nodes +func _on_graph_node_recompiled(_graph_node): + if refreshed: return + $GraphEdit.clear_connections() + for graph_node in $GraphEdit.get_children(): + if not graph_node is GraphNode: + continue + + var connections = graph_node.get_connections() + graph_node.set_slot_enabled_right(0, connections.size() != 0) + graph_node.set_slot_type_right(0, 1) + + for connection in connections: + if not $GraphEdit.has_node(connection): + continue + + var other_graph_node = $GraphEdit.get_node(connection) + other_graph_node.set_slot_enabled_left(0, true) + other_graph_node.set_slot_type_left(0, 1) + + $GraphEdit.connect_node(graph_node.name, 0, connection, 0) + + refreshed = true + yield(get_tree().create_timer(.3), 'timeout') + refreshed = false + func _on_graph_node_input(event, graph_node, node): if event is InputEventMouseButton \ and event.doubleclick and event.button_index == BUTTON_LEFT: diff --git a/addons/Wol/editor/WolGraphNode.gd b/addons/Wol/editor/WolGraphNode.gd index 1ef08c3..c34992a 100644 --- a/addons/Wol/editor/WolGraphNode.gd +++ b/addons/Wol/editor/WolGraphNode.gd @@ -4,11 +4,13 @@ extends GraphNode signal recompiled const Compiler = preload('res://addons/Wol/core/compiler/Compiler.gd') +const Constants = preload('res://addons/Wol/core/Constants.gd') var node setget set_node var error_lines = [] var compiler +var program onready var text_edit = $TextEdit @@ -18,7 +20,32 @@ func _ready(): $TextDebounce.connect('timeout', self, '_on_debounce') func get_connections(): - print(compiler) + # NOTE: Program failed to compile + if not program: + return [] + + var program_node = program.nodes.values().front() + var connections = [] + + # FIXME: Don't rely on checking on 'Label\d' + var label_check = RegEx.new() + label_check.compile('^Label\\doption_\\d') + + for instruction in program_node.instructions: + # NOTE: When next node is explicit + if instruction.operation == Constants.ByteCode.RunNode: + if instruction.operands.size() > 0 \ + and instruction.operands.front().value != name: + connections.append(instruction.operands.front().value) + + # NOTE: When next node is decided through options + if instruction.operation == Constants.ByteCode.AddOption: + if instruction.operands.size() == 2 \ + and not label_check.search(instruction.operands[1].value) \ + and instruction.operands[1].value != name: + connections.append(instruction.operands[1].value) + + return connections func _on_text_changed(): $TextDebounce.start(.3) @@ -45,6 +72,7 @@ func _on_error(message, line_number, _column): func set_node(_node): node = _node title = node.title + name = node.title text_edit.text = node.body text_edit.clear_undo_history() offset = node.position @@ -55,5 +83,7 @@ func compile(): var text = '---\n%s\n===' % text_edit.text compiler = Compiler.new(null, text, true) compiler.connect('error', self, '_on_error') - compiler.compile() + program = compiler.compile() + + yield(get_tree(), 'idle_frame') emit_signal('recompiled') From c1bcd2469432a7b5f870b53260cab6feea5360d1 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 6 Dec 2021 22:24:06 +0100 Subject: [PATCH 14/19] Added some more security to the runtime --- addons/Wol/core/compiler/Parser.gd | 6 +++- addons/Wol/editor/Editor.gd | 10 ++++-- addons/Wol/editor/WolEditor.gd | 49 ++++++++++++++++++++++++++++++ addons/Wol/editor/WolEditor.tscn | 4 +-- addons/Wol/editor/WolGraphNode.gd | 2 +- dialogue.wol | 24 +++++++-------- 6 files changed, 77 insertions(+), 18 deletions(-) diff --git a/addons/Wol/core/compiler/Parser.gd b/addons/Wol/core/compiler/Parser.gd index 4beacbd..1ea9cd6 100644 --- a/addons/Wol/core/compiler/Parser.gd +++ b/addons/Wol/core/compiler/Parser.gd @@ -25,7 +25,7 @@ func parse_node(): return WolNode.new('Start', null, self) func next_symbol_is(valid_types): - compiler.assert(tokens.size() != 0, 'Ran out of tokens!') + compiler.assert(tokens.size() != 0, 'Ran out of tokens looking for next symbol!') return tokens.front() and tokens.front().type in valid_types # NOTE: 0 look ahead for `<<` and `else` @@ -37,8 +37,12 @@ func next_symbols_are(valid_types): return true func expect_symbol(token_types = []): + compiler.assert(tokens.size() != 0, 'Ran out of tokens expecting next symbol!') var token = tokens.pop_front() as Lexer.Token + if tokens.size() == 0: + return token + if token_types.size() == 0: if token.type == Constants.TokenType.EndOfInput: compiler.assert(false, 'Unexpected end of input') diff --git a/addons/Wol/editor/Editor.gd b/addons/Wol/editor/Editor.gd index 3b1a108..3cc120b 100644 --- a/addons/Wol/editor/Editor.gd +++ b/addons/Wol/editor/Editor.gd @@ -21,9 +21,11 @@ func open_node(graph_node, node): $Content.add_child(text_edit) toggle_text_edit(text_edit) - show() + $HBoxContainer/TextEdit.disconnect('text_changed', self, '_on_title_changed') + $HBoxContainer/TextEdit.text = node.title + $HBoxContainer/TextEdit.connect('text_changed', self, '_on_title_changed') - # window_title = node.title + show() func toggle_text_edit(text_edit): text_edit.anchor_left = 0 @@ -48,6 +50,10 @@ func toggle_text_edit(text_edit): ]: text_edit.set(property, not text_edit.get(property)) +func _on_title_changed(): + current_graph_node.node.title = $HBoxContainer/TextEdit.text + current_graph_node.compile() + func _on_visibility_changed(): if not visible: var text_edit = $Content/TextEdit diff --git a/addons/Wol/editor/WolEditor.gd b/addons/Wol/editor/WolEditor.gd index 0479984..7128090 100644 --- a/addons/Wol/editor/WolEditor.gd +++ b/addons/Wol/editor/WolEditor.gd @@ -6,16 +6,46 @@ onready var GraphNodeTemplate = $GraphNodeTemplate var path var refreshed = false +var selected_node + +onready var original_delete_node_dialog = $DeleteNodeDialog.dialog_text func _ready(): for menu_button in [$Menu/File]: menu_button.get_popup().connect('index_pressed', self, '_on_menu_pressed', [menu_button.get_popup()]) + $GraphEdit.connect('gui_input', self, '_on_graph_edit_input') + $GraphEdit.connect('node_selected', self, '_on_node_selected', [true]) + $GraphEdit.connect('node_unselected', self, '_on_node_selected', [false]) + + $DeleteNodeDialog.connect('confirmed', self, 'delete_node') + # TODO: Conditionally load in theme based on Editor or standalone path = 'res://dialogue.wol' build_nodes() +func create_node(position = Vector2.ZERO): + print('creating node!') + var graph_node = GraphNodeTemplate.duplicate() + $GraphEdit.add_child(graph_node) + + var node = { + 'title': 'NewNode', + 'body': 'Wol: Hello world', + 'position': position + } + + graph_node.connect('recompiled', self, '_on_graph_node_recompiled', [graph_node]) + graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node, node]) + + graph_node.node = node + graph_node.show() + +func delete_node(node = selected_node): + $GraphEdit.remove_child(node) + node.queue_free() + func build_nodes(): for node in Compiler.new(path).get_nodes(): var graph_node = GraphNodeTemplate.duplicate() @@ -128,3 +158,22 @@ func _on_graph_node_input(event, graph_node, node): and event.doubleclick and event.button_index == BUTTON_LEFT: $HBoxContainer/Editor.open_node(graph_node, node) accept_event() + +func _on_node_selected(node, selected): + if not selected: + selected_node = null + else: + selected_node = node + +func _on_graph_edit_input(event): + if event is InputEventMouseButton \ + and event.doubleclick and event.button_index == BUTTON_LEFT: + create_node(event.global_position + $GraphEdit.scroll_offset) + +func _input(event): + if event is InputEventKey \ + and not event.pressed and event.scancode == KEY_DELETE \ + and selected_node: + $DeleteNodeDialog.dialog_text = original_delete_node_dialog % selected_node.name + $DeleteNodeDialog.popup() + diff --git a/addons/Wol/editor/WolEditor.tscn b/addons/Wol/editor/WolEditor.tscn index e9eac17..88db627 100644 --- a/addons/Wol/editor/WolEditor.tscn +++ b/addons/Wol/editor/WolEditor.tscn @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8967c8b3e310a5ab4a5c78cf3d4f3b3a94fbcd4e02ed5c736c413e4106cf9249 -size 604382 +oid sha256:c726947550552cda030c91eb876dbbb8d2840d9faeba9b90a6f428a43db140f4 +size 605185 diff --git a/addons/Wol/editor/WolGraphNode.gd b/addons/Wol/editor/WolGraphNode.gd index c34992a..d70dceb 100644 --- a/addons/Wol/editor/WolGraphNode.gd +++ b/addons/Wol/editor/WolGraphNode.gd @@ -80,7 +80,7 @@ func set_node(_node): compile() func compile(): - var text = '---\n%s\n===' % text_edit.text + var text = 'title: %s\n---\n%s\n===' % [node.title, text_edit.text] compiler = Compiler.new(null, text, true) compiler.connect('error', self, '_on_error') program = compiler.compile() diff --git a/dialogue.wol b/dialogue.wol index 869e4b6..d6006c6 100644 --- a/dialogue.wol +++ b/dialogue.wol @@ -28,18 +28,6 @@ Guy: WHY ARE YOU BUYING CLOTHES AT THE SOUP STORE?! You: FUCK YOU! [[Go home|Start]] === -title: Talk -tags: -colorID: -position: 800, 400 ---- -Narrator: So how are you really? -You: I'm good! -Narrator: Do you want to continue talking? --> Yes - [[Start]] --> No -=== title: Start tags: colorID: @@ -67,4 +55,16 @@ Narrator: You wanna go somewhere? [[Start]] -> Lets stay here and talk [[Talk]] +=== +title: Talk +tags: +colorID: +position: 800, 400 +--- +Narrator: So how are you really? +You: I'm good! +Narrator: Do you want to continue talking? +-> Yes + [[Start]] +-> No === \ No newline at end of file From 753a50460420940742517a94f03e58d25a3343cd Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Tue, 7 Dec 2021 10:07:11 +0100 Subject: [PATCH 15/19] made mistakes more safe (no more infinite loops) --- addons/Wol/core/compiler/Compiler.gd | 2 + addons/Wol/core/compiler/Parser.gd | 57 +++++++++++++++------------- addons/Wol/editor/WolGraphNode.gd | 2 +- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/addons/Wol/core/compiler/Compiler.gd b/addons/Wol/core/compiler/Compiler.gd index 55a4a62..d705ec2 100644 --- a/addons/Wol/core/compiler/Compiler.gd +++ b/addons/Wol/core/compiler/Compiler.gd @@ -130,6 +130,8 @@ func assert(statement, message, line_number = -1, column = -1, _absolute_line_nu elif not statement: emit_signal('error', message, line_number, column) + return not statement + func compile(): var parsed_nodes = [] for node in get_nodes(): diff --git a/addons/Wol/core/compiler/Parser.gd b/addons/Wol/core/compiler/Parser.gd index 1ea9cd6..da66de5 100644 --- a/addons/Wol/core/compiler/Parser.gd +++ b/addons/Wol/core/compiler/Parser.gd @@ -37,15 +37,13 @@ func next_symbols_are(valid_types): return true func expect_symbol(token_types = []): - compiler.assert(tokens.size() != 0, 'Ran out of tokens expecting next symbol!') + if compiler.assert(tokens.size() != 0, 'Ran out of tokens expecting next symbol!'): + return + var token = tokens.pop_front() as Lexer.Token - if tokens.size() == 0: - return token - if token_types.size() == 0: - if token.type == Constants.TokenType.EndOfInput: - compiler.assert(false, 'Unexpected end of input') + compiler.assert(token.type != Constants.TokenType.EndOfInput, 'Unexpected end of input') return token for type in token_types: @@ -70,7 +68,7 @@ func expect_symbol(token_types = []): error_guess ] compiler.assert(false, 'Expected token "%s" but got "%s"%s' % error_data, token.line_number, token.column) - return token + return static func tab(indent_level, input, newline = true): return '%*s| %s%s' % [indent_level * 2, '', input, '' if not newline else '\n'] @@ -127,8 +125,12 @@ class WolNode extends ParseNode: parser.tokens.front().line_number, parser.tokens.front().column ) + + var statement = Statement.new(self, parser) + if statement.failed_to_parse: + break - statements.append(Statement.new(self, parser)) + statements.append(statement) func tree_string(indent_level): var info = [] @@ -148,6 +150,7 @@ class Statement extends ParseNode: var shortcut_option_group var custom_command var line + var failed_to_parse = false func _init(parent, parser).(parent, parser): if Block.can_parse(parser): @@ -180,6 +183,8 @@ class Statement extends ParseNode: else: parser.compiler.assert(false, 'Expected a statement but got %s instead. (probably an imbalanced if statement)' % parser.tokens.front()._to_string()) + failed_to_parse = true + return var tags = [] @@ -210,7 +215,7 @@ class Statement extends ParseNode: Type.Line: info.append(line.tree_string(indent_level)) _: - printerr('Cannot print statement') + self.parser.compiler.assert(false, 'Cannot print statement') return PoolStringArray(info).join('') @@ -297,7 +302,7 @@ class LineNode extends ParseNode: if line_id.empty(): line_id = tag_token.value else: - printerr("Too many line_tags @[%s:%d]" % [parser.currentNodeName, tag_token.line_number]) + parser.compiler.assert(false, 'Too many line_tags @[%s:%d]' % [parser.currentNodeName, tag_token.line_number]) return else: tags.append(tag_token.value) @@ -642,7 +647,7 @@ class ValueNode extends ParseNode: Constants.TokenType.NullToken: value = Value.new(null) _: - printerr('%s, Invalid token type' % token.name) + self.parser.compiler.assert(false, '%s, Invalid token type' % token.name) func tree_string(indent_level): return tab(indent_level, '%s' % value.value()) @@ -736,16 +741,15 @@ class ExpressionNode extends ParseNode: while op_stack.back().type != Constants.TokenType.LeftParen: var p = op_stack.pop_back() if p == null: - printerr('unbalanced parenthesis %s' % next.name) + parser.compiler.assert(false, 'unbalanced parenthesis %s' % next.name) break rpn.append(p) #next token in op_stack left paren # next parser token not allowed to be right paren or comma - if parser.next_symbol_is([Constants.TokenType.RightParen, - Constants.TokenType.Comma]): - printerr('Expected Expression : %s' % parser.tokens.front().name) + if parser.next_symbol_is([Constants.TokenType.RightParen, Constants.TokenType.Comma]): + parser.compiler.assert(false, 'Expected Expression : %s' % parser.tokens.front().name) #find the closest function on stack #increment parameters @@ -823,7 +827,7 @@ class ExpressionNode extends ParseNode: #if rpn is empty then this is not expression if rpn.size() == 0: - printerr('Error parsing expression: Expression not found!') + parser.compiler.assert(false, 'Error parsing expression: Expression not found!') #build expression tree var first = rpn.front() @@ -833,10 +837,10 @@ class ExpressionNode extends ParseNode: var next = rpn.pop_front() if Operator.is_op(next.type): #operation - var info = Operator.op_info(next.type) + var info = Operator.op_info(next.type, parser) if eval_stack.size() < info.arguments: - printerr( + parser.compiler.assert(false, 'Error parsing : Not enough arguments for %s [ got %s expected - was %s]' \ % [ Constants.token_type_name(next.type), @@ -900,8 +904,7 @@ class ExpressionNode extends ParseNode: if operator_stack.size() == 0: return false - if not Operator.is_op(_type): - parser.compiler.assert(false, 'Unable to parse expression!') + if parser.compiler.assert(Operator.is_op(_type), 'Unable to parse expression!'): return false var second = operator_stack.back().type @@ -909,8 +912,8 @@ class ExpressionNode extends ParseNode: if not Operator.is_op(second): return false - var first_info = Operator.op_info(_type) - var second_info = Operator.op_info(second) + var first_info = Operator.op_info(_type, parser) + var second_info = Operator.op_info(second, parser) return \ (first_info.associativity == Associativity.Left \ @@ -970,9 +973,9 @@ class Operator extends ParseNode: info.append(tab(indent_level, op_type)) return info.join('') - static func op_info(op): - if not Operator.is_op(op) : - printerr('%s is not a valid operator' % op.name) + static func op_info(op, parser): + if parser.compiler.assert(Operator.is_op(op), '%s is not a valid operator' % op): + return #determine associativity and operands # each operand has @@ -996,8 +999,8 @@ class Operator extends ParseNode: TokenType.Xor: return OperatorInfo.new(Associativity.Left, 2, 2) _: - printerr('Unknown operator: %s' % op.name) - return null + parser.compiler.assert(false, 'Unknown operator: %s' % op.name) + return static func is_op(type): return type in op_types() diff --git a/addons/Wol/editor/WolGraphNode.gd b/addons/Wol/editor/WolGraphNode.gd index d70dceb..3d85348 100644 --- a/addons/Wol/editor/WolGraphNode.gd +++ b/addons/Wol/editor/WolGraphNode.gd @@ -48,7 +48,7 @@ func get_connections(): return connections func _on_text_changed(): - $TextDebounce.start(.3) + $TextDebounce.start() func _on_debounce(): text_edit.get_node('ErrorGutter').hide() From a57f2193d91979fa12d002c6f726252436921b7b Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Tue, 7 Dec 2021 10:33:03 +0100 Subject: [PATCH 16/19] Fixed another crash --- addons/Wol/core/compiler/Compiler.gd | 3 +++ addons/Wol/core/compiler/Parser.gd | 20 +++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/addons/Wol/core/compiler/Compiler.gd b/addons/Wol/core/compiler/Compiler.gd index d705ec2..82996ff 100644 --- a/addons/Wol/core/compiler/Compiler.gd +++ b/addons/Wol/core/compiler/Compiler.gd @@ -412,6 +412,9 @@ func generate_assignment(node, assignment): emit(Constants.ByteCode.Pop, node) func generate_expression(node, expression): + if self.assert(expression != null, 'Wrong expression (perhaps unterminated command block ">>"?)'): + return false + match expression.type: Constants.ExpressionType.Value: generate_value(node, expression.value) diff --git a/addons/Wol/core/compiler/Parser.gd b/addons/Wol/core/compiler/Parser.gd index da66de5..526b569 100644 --- a/addons/Wol/core/compiler/Parser.gd +++ b/addons/Wol/core/compiler/Parser.gd @@ -25,7 +25,16 @@ func parse_node(): return WolNode.new('Start', null, self) func next_symbol_is(valid_types): - compiler.assert(tokens.size() != 0, 'Ran out of tokens looking for next symbol!') + if tokens.size() == 0: + var error_tokens = [] + for token in valid_types: + error_tokens.append(Constants.token_name(token)) + + if error_tokens == ['TagMarker']: + error_tokens.append('OptionEnd') + + compiler.assert(tokens.size() != 0, 'Ran out of tokens looking for next symbol "%s"!' % PoolStringArray(error_tokens).join(', ')) + return tokens.front() and tokens.front().type in valid_types # NOTE: 0 look ahead for `<<` and `else` @@ -734,7 +743,8 @@ class ExpressionNode extends ParseNode: #next token is parent - left next = parser.expect_symbol([Constants.TokenType.LeftParen]) - op_stack.push_back(next) + if next: + op_stack.push_back(next) elif next.type == Constants.TokenType.Comma: #resolve sub expression before moving on @@ -906,7 +916,11 @@ class ExpressionNode extends ParseNode: if parser.compiler.assert(Operator.is_op(_type), 'Unable to parse expression!'): return false - + + # FIXME: Make sure there can't be a Null value here + if parser.compiler.assert(operator_stack.back() != null, 'Something went wrong getting precedence'): + return false + var second = operator_stack.back().type if not Operator.is_op(second): From b8eb90cbf778e02a5e4751effa045e54ba66069f Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Tue, 7 Dec 2021 12:12:33 +0100 Subject: [PATCH 17/19] feat: Added a pane for previewing dialogue --- ExampleDialogue/Sally.yarn | 63 +++++++++++------------ addons/Wol/Wol.gd | 8 +++ addons/Wol/core/compiler/Compiler.gd | 6 +-- addons/Wol/editor/Editor.gd | 19 ++++--- addons/Wol/editor/Preview.gd | 76 ++++++++++++++++++++++++++++ addons/Wol/editor/WolEditor.gd | 11 ++-- addons/Wol/editor/WolEditor.tscn | 4 +- 7 files changed, 138 insertions(+), 49 deletions(-) create mode 100644 addons/Wol/editor/Preview.gd diff --git a/ExampleDialogue/Sally.yarn b/ExampleDialogue/Sally.yarn index 0101a35..5147c7d 100644 --- a/ExampleDialogue/Sally.yarn +++ b/ExampleDialogue/Sally.yarn @@ -1,7 +1,35 @@ +title: Sally.Watch +tags: +colorID: +position: 500, 400 +--- +Sally: Not really. #line:8c3f98 +Sally: Same old nebula, doing the same old thing. #line:24c418 +Sally: Oh, Ship wanted to see you. Go say hi to it. #line:df4eaf +<> +<> + Player: Already done! #line:1fea6c + Sally: Go say hi again. #line:5df323 +<> +=== +title: Sally.Exit +tags: +colorID: +position: 100, 400 +--- +Sally: Bye. #line:60c282 +=== +title: Sally.Sorry +tags: +colorID: +position: 900, 400 +--- +Sally: Yeah. Don't do it again. #line:d7df49 +=== title: Sally tags: -colorID: 0 -position: 524,111 +colorID: +position: 500, 100 --- <> Player: Hey, Sally. #line:794945 @@ -22,34 +50,3 @@ position: 524,111 <> [[See you later.|Sally.Exit]] #line:0facf7 === - -title: Sally.Watch -tags: -colorID: 0 -position: 512,430 ---- -Sally: Not really. #line:8c3f98 -Sally: Same old nebula, doing the same old thing. #line:24c418 -Sally: Oh, Ship wanted to see you. Go say hi to it. #line:df4eaf -<> -<> - Player: Already done! #line:1fea6c - Sally: Go say hi again. #line:5df323 -<> -=== - -title: Sally.Exit -tags: -colorID: 6 -position: 211,417 ---- -Sally: Bye. #line:60c282 -=== - -title: Sally.Sorry -tags: -colorID: 0 -position: 827,439 ---- -Sally: Yeah. Don't do it again. #line:d7df49 -=== diff --git a/addons/Wol/Wol.gd b/addons/Wol/Wol.gd index 8b20d44..2405459 100644 --- a/addons/Wol/Wol.gd +++ b/addons/Wol/Wol.gd @@ -55,6 +55,9 @@ func set_path(_path): var compiler = Compiler.new(path) virtual_machine.program = compiler.compile() +func set_program(program): + virtual_machine.program = program + func _on_line(line): if auto_substitute: var index = 0 @@ -106,5 +109,10 @@ func start(node = starting_node): virtual_machine.set_node(node) virtual_machine.start() +func stop(): + if running: + virtual_machine.call_deferred('stop') + running = false + func resume(): virtual_machine.call_deferred('resume') diff --git a/addons/Wol/core/compiler/Compiler.gd b/addons/Wol/core/compiler/Compiler.gd index 82996ff..3040453 100644 --- a/addons/Wol/core/compiler/Compiler.gd +++ b/addons/Wol/core/compiler/Compiler.gd @@ -1,5 +1,4 @@ extends Object - signal error(message, line_number, column) const Constants = preload('res://addons/Wol/core/Constants.gd') @@ -25,11 +24,12 @@ func _init(_filename, _source = null, _soft_assert = false): soft_assert = _soft_assert if not _filename and _source: - self.source = _source + filename = 'inline_source' + source = _source else: var file = File.new() file.open(_filename, File.READ) - self.source = file.get_as_text() + source = file.get_as_text() file.close() var source_lines = source.split('\n') diff --git a/addons/Wol/editor/Editor.gd b/addons/Wol/editor/Editor.gd index 3cc120b..47aac83 100644 --- a/addons/Wol/editor/Editor.gd +++ b/addons/Wol/editor/Editor.gd @@ -1,19 +1,21 @@ tool extends Panel -var current_node var current_graph_node +onready var preview = get_node('../Preview') + func _ready(): hide() connect('visibility_changed', self, '_on_visibility_changed') - $Close.connect('pressed', self, 'close') + $Tools/Left/Play.connect('pressed', self, '_on_play') + $Tools/Right/Close.connect('pressed', self, 'close') func close(): hide() + preview.close() -func open_node(graph_node, node): - current_node = node +func open_node(graph_node): current_graph_node = graph_node var text_edit = graph_node.get_node('TextEdit') @@ -21,9 +23,9 @@ func open_node(graph_node, node): $Content.add_child(text_edit) toggle_text_edit(text_edit) - $HBoxContainer/TextEdit.disconnect('text_changed', self, '_on_title_changed') - $HBoxContainer/TextEdit.text = node.title - $HBoxContainer/TextEdit.connect('text_changed', self, '_on_title_changed') + $Tools/Left/Title.disconnect('text_changed', self, '_on_title_changed') + $Tools/Left/Title.text = graph_node.node.title + $Tools/Left/Title.connect('text_changed', self, '_on_title_changed') show() @@ -50,6 +52,9 @@ func toggle_text_edit(text_edit): ]: text_edit.set(property, not text_edit.get(property)) +func _on_play(): + preview.open_node(current_graph_node) + func _on_title_changed(): current_graph_node.node.title = $HBoxContainer/TextEdit.text current_graph_node.compile() diff --git a/addons/Wol/editor/Preview.gd b/addons/Wol/editor/Preview.gd new file mode 100644 index 0000000..39f0991 --- /dev/null +++ b/addons/Wol/editor/Preview.gd @@ -0,0 +1,76 @@ +extends Panel + +var current_graph_node + +onready var line_template = $Content/List/LineTemplate +onready var button_template = $Options/List/ButtonTemplate + +onready var wol_editor = find_parent('WolEditor') + +func _ready(): + hide() + $Wol.connect('line', self, '_on_line') + $Wol.connect('options', self, '_on_options') + $Tools/Right/Close.connect('pressed', self, 'close') + +func open_node(graph_node): + current_graph_node = graph_node + + $Wol.stop() + + for child in $Content/List.get_children(): + if child != line_template and not 'Padding' in child.name: + $Content/List.remove_child(child) + child.queue_free() + + for child in $Options/List.get_children(): + if child != button_template: + $Options/List.remove_child(child) + child.queue_free() + + $Wol.variable_storage = {} + $Wol.set_program(wol_editor.get_program()) + $Wol.start(current_graph_node.node.title) + show() + +func close(): + hide() + current_graph_node = null + $Wol.stop() + +func next(): + $Wol.resume() + +func _on_line(line): + var line_node = line_template.duplicate() + $Content/List.add_child(line_node) + $Content/List/PaddingBottom.raise() + line_node.get_node('RichTextLabel').bbcode_text = line.text + line_node.show() + yield(get_tree(), 'idle_frame') + $Content.scroll_vertical = $Content/List.rect_size.y + +func _on_options(options): + for option in options: + var button = button_template.duplicate() + $Options/List.add_child(button) + + button.text = option.line.text + button.connect('pressed', self, '_on_option_pressed', [option]) + button.show() + +func _on_option_pressed(option): + $Wol.select_option(option.id) + + for child in $Options/List.get_children(): + if child != button_template: + $Options/List.remove_child(child) + child.queue_free() + +func _input(event): + if visible \ + and ( \ + (event is InputEventMouseButton and event.doubleclick) \ + or (event is InputEventKey and not event.pressed and event.scancode in [KEY_SPACE, KEY_ENTER]) \ + ): + next() diff --git a/addons/Wol/editor/WolEditor.gd b/addons/Wol/editor/WolEditor.gd index 7128090..5b4c891 100644 --- a/addons/Wol/editor/WolEditor.gd +++ b/addons/Wol/editor/WolEditor.gd @@ -37,7 +37,7 @@ func create_node(position = Vector2.ZERO): } graph_node.connect('recompiled', self, '_on_graph_node_recompiled', [graph_node]) - graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node, node]) + graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node]) graph_node.node = node graph_node.show() @@ -52,11 +52,14 @@ func build_nodes(): $GraphEdit.add_child(graph_node) graph_node.connect('recompiled', self, '_on_graph_node_recompiled', [graph_node]) - graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node, node]) + graph_node.connect('gui_input', self, '_on_graph_node_input', [graph_node]) graph_node.node = node graph_node.show() +func get_program(): + return Compiler.new(null, serialize_to_file(), true).compile() + func serialize_to_file(): var buffer = [] for graph_node in $GraphEdit.get_children(): @@ -153,10 +156,10 @@ func _on_graph_node_recompiled(_graph_node): yield(get_tree().create_timer(.3), 'timeout') refreshed = false -func _on_graph_node_input(event, graph_node, node): +func _on_graph_node_input(event, graph_node): if event is InputEventMouseButton \ and event.doubleclick and event.button_index == BUTTON_LEFT: - $HBoxContainer/Editor.open_node(graph_node, node) + $HBoxContainer/Editor.open_node(graph_node) accept_event() func _on_node_selected(node, selected): diff --git a/addons/Wol/editor/WolEditor.tscn b/addons/Wol/editor/WolEditor.tscn index 88db627..29f694f 100644 --- a/addons/Wol/editor/WolEditor.tscn +++ b/addons/Wol/editor/WolEditor.tscn @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c726947550552cda030c91eb876dbbb8d2840d9faeba9b90a6f428a43db140f4 -size 605185 +oid sha256:ef446d59a4c2bd4cf042b55abf92d9c816c2af021f81e6c8f6fd83a07ba75a55 +size 610584 From f30f8ad921e6228f0678d62f1f8b931cfc1c6b40 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Tue, 7 Dec 2021 12:16:36 +0100 Subject: [PATCH 18/19] docs: Added development notice --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6aabd33..accd4ea 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ --- -**Wol** is a tool for creating interactive dialogue for games. Its based on [YarnSpinner](https://yarnspinner.dev/) and it's [Yarn language](https://yarnspinner.dev/docs/syntax/). +**Wol** is a tool for creating interactive dialogue for games. Its based on [YarnSpinner](https://yarnspinner.dev/) and it's [Yarn language](https://yarnspinner.dev/docs/syntax/). (**_currently under heavy development, very unstable, do not use for production_**) Write your conversations in *Yarn*, a simple programming language that's designed to be easy for writers to learn, while also powerful enough to handle whatever you need. From d457c38974f34bbda8fb771682c8c4547fcb5fdc Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Tue, 7 Dec 2021 12:33:49 +0100 Subject: [PATCH 19/19] docs: added a bunch of TODOs --- addons/Wol/editor/Preview.gd | 6 ++++++ addons/Wol/editor/WolEditor.gd | 3 ++- addons/Wol/editor/WolGraphNode.gd | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/Wol/editor/Preview.gd b/addons/Wol/editor/Preview.gd index 39f0991..d0d8503 100644 --- a/addons/Wol/editor/Preview.gd +++ b/addons/Wol/editor/Preview.gd @@ -7,6 +7,10 @@ onready var button_template = $Options/List/ButtonTemplate onready var wol_editor = find_parent('WolEditor') +# TODO: Make sure all focus is lost when clicking this pane +# TODO: Add restart button +# TODO: Add next button + func _ready(): hide() $Wol.connect('line', self, '_on_line') @@ -45,6 +49,8 @@ func _on_line(line): var line_node = line_template.duplicate() $Content/List.add_child(line_node) $Content/List/PaddingBottom.raise() + # TODO: Add hash() based color from speaker + line_node.get_node('RichTextLabel').bbcode_text = line.text line_node.show() yield(get_tree(), 'idle_frame') diff --git a/addons/Wol/editor/WolEditor.gd b/addons/Wol/editor/WolEditor.gd index 5b4c891..174b5b2 100644 --- a/addons/Wol/editor/WolEditor.gd +++ b/addons/Wol/editor/WolEditor.gd @@ -176,7 +176,8 @@ func _on_graph_edit_input(event): func _input(event): if event is InputEventKey \ and not event.pressed and event.scancode == KEY_DELETE \ - and selected_node: + and selected_node \ + and not $HBoxContainer/Editor.visible: $DeleteNodeDialog.dialog_text = original_delete_node_dialog % selected_node.name $DeleteNodeDialog.popup() diff --git a/addons/Wol/editor/WolGraphNode.gd b/addons/Wol/editor/WolGraphNode.gd index 3d85348..67db7b1 100644 --- a/addons/Wol/editor/WolGraphNode.gd +++ b/addons/Wol/editor/WolGraphNode.gd @@ -14,6 +14,8 @@ var program onready var text_edit = $TextEdit +# TODO: Add syntax highlighting + func _ready(): connect('offset_changed', self, '_on_offset_changed') text_edit.connect('text_changed', self, '_on_text_changed')